From: ToineSiebelink Date: Thu, 8 Jan 2026 13:22:35 +0000 (+0000) Subject: Fix bugs for Policy Executor requests: X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=refs%2Fchanges%2F95%2F142895%2F2;p=cps.git Fix bugs for Policy Executor requests: - applied naming convention, main ones: requestParameters.fdn() For fdn from URI and or extendedFdn in case of patch -operation (extended)path for anything that might have attributes only use terms "resourceIdentifier" and "targetIdentifier" JUST before calling Policy Execution method - Ensure path for each patch operation is appended to target FDN - DeleteOperationDetails is not needed, using blank default instead - So only one type of OperationDetails required, so removed interface and renamed the only impl. - Refactored code to correctly handle possible child paths in patchItems - hardcoded attribute conversion to object for standard /attributes without # - objectName in body for patch is optional, should be same as in URI, id should be same too. So we CAN ignore (no validation!) - added convenience method for getting parent or targetFdn (when /attributes is used) - added convenience method for removing trailing # (when #/attributes is used) - renamed ParameterMapper to Helper as it is more generic now. Also made is static for easier access TODO - Check URL send to DMI (test!) when using (#)/attributes Decisions Csaba K (to record in Wiki) - (#)/attributes is NOT allowed in get or create or delete - (#)/attributes is NOT compulsory for Patch.replace and Patch.uppate - (#)/attributes should NOT be in URI to PolicyExecutor (targetIdentifier) Issue-ID: CPS-2826 Change-Id: Icd1dbcd6033b79019d35d6cb68c1d6caee64fb01 Signed-off-by: ToineSiebelink --- 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 128ad92c63..881dfff82c 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 @@ -21,8 +21,9 @@ package org.onap.cps.ncmp.rest.controller; import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE; +import static org.onap.cps.ncmp.impl.data.policyexecutor.OperationDetailsFactory.DELETE_OPERATION_DETAILS; import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA; -import static org.onap.cps.ncmp.impl.provmns.ParameterMapper.NO_OP; +import static org.onap.cps.ncmp.impl.provmns.ParameterHelper.NO_OP; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.CONFLICT; import static org.springframework.http.HttpStatus.GATEWAY_TIMEOUT; @@ -43,15 +44,13 @@ import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException; import org.onap.cps.ncmp.api.exceptions.ProvMnSException; import org.onap.cps.ncmp.api.inventory.models.CmHandleState; import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException; -import org.onap.cps.ncmp.impl.data.policyexecutor.CreateOperationDetails; -import org.onap.cps.ncmp.impl.data.policyexecutor.DeleteOperationDetails; import org.onap.cps.ncmp.impl.data.policyexecutor.OperationDetails; import org.onap.cps.ncmp.impl.data.policyexecutor.OperationDetailsFactory; 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.ParameterMapper; +import org.onap.cps.ncmp.impl.provmns.ParameterHelper; import org.onap.cps.ncmp.impl.provmns.ParametersBuilder; import org.onap.cps.ncmp.impl.provmns.RequestParameters; import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdGetDataNodeSelectorParameter; @@ -82,7 +81,6 @@ public class ProvMnSController implements ProvMnS { private final DmiRestClient dmiRestClient; private final InventoryPersistence inventoryPersistence; private final ParametersBuilder parametersBuilder; - private final ParameterMapper parameterMapper; private final PolicyExecutor policyExecutor; private final JsonObjectMapper jsonObjectMapper; private final OperationDetailsFactory operationDetailsFactory; @@ -97,12 +95,11 @@ public class ProvMnSController implements ProvMnS { final List attributes, final List fields, final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector) { - final RequestParameters requestParameters = parameterMapper.extractRequestParameters(httpServletRequest); + final RequestParameters requestParameters = ParameterHelper.extractRequestParameters(httpServletRequest); try { final YangModelCmHandle yangModelCmHandle = getAndValidateYangModelCmHandle(requestParameters); - final String targetFdn = requestParameters.toTargetFdn(); final UrlTemplateParameters urlTemplateParameters = parametersBuilder.createUrlTemplateParametersForRead( - yangModelCmHandle, targetFdn, scope, filter, attributes, fields, dataNodeSelector); + yangModelCmHandle, requestParameters.fdn(), scope, filter, attributes, fields, dataNodeSelector); return dmiRestClient.synchronousGetOperation(DATA, urlTemplateParameters); } catch (final Exception exception) { throw toProvMnSException(httpServletRequest.getMethod(), exception, NO_OP); @@ -117,13 +114,12 @@ public class ProvMnSController implements ProvMnS { + maxNumberOfPatchOperations; throw new ProvMnSException(httpServletRequest.getMethod(), PAYLOAD_TOO_LARGE, title, NO_OP); } - final RequestParameters requestParameters = parameterMapper.extractRequestParameters(httpServletRequest); + final RequestParameters requestParameters = ParameterHelper.extractRequestParameters(httpServletRequest); try { final YangModelCmHandle yangModelCmHandle = getAndValidateYangModelCmHandle(requestParameters); - checkPermissionForEachPatchItem(requestParameters, patchItems, yangModelCmHandle); - final String targetFdn = requestParameters.toTargetFdn(); + checkPermissionForEachPatchItem(requestParameters.fdn(), patchItems, yangModelCmHandle); final UrlTemplateParameters urlTemplateParameters = - parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, targetFdn); + parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestParameters.fdn()); return dmiRestClient.synchronousPatchOperation(DATA, patchItems, urlTemplateParameters, httpServletRequest.getContentType()); } catch (final Exception exception) { @@ -133,15 +129,14 @@ public class ProvMnSController implements ProvMnS { @Override public ResponseEntity putMoi(final HttpServletRequest httpServletRequest, final Resource resource) { - final RequestParameters requestParameters = parameterMapper.extractRequestParameters(httpServletRequest); + final RequestParameters requestParameters = ParameterHelper.extractRequestParameters(httpServletRequest); try { final YangModelCmHandle yangModelCmHandle = getAndValidateYangModelCmHandle(requestParameters); - final CreateOperationDetails createOperationDetails = - operationDetailsFactory.buildCreateOperationDetails(CREATE, requestParameters, resource); - checkPermission(yangModelCmHandle, requestParameters.toTargetFdn(), createOperationDetails); - final String targetFdn = requestParameters.toTargetFdn(); + final OperationDetails operationDetails = + operationDetailsFactory.buildOperationDetails(CREATE, requestParameters, resource); + checkPermission(yangModelCmHandle, requestParameters.fdn(), operationDetails); final UrlTemplateParameters urlTemplateParameters = - parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, targetFdn); + parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestParameters.fdn()); return dmiRestClient.synchronousPutOperation(DATA, resource, urlTemplateParameters); } catch (final Exception exception) { throw toProvMnSException(httpServletRequest.getMethod(), exception, NO_OP); @@ -150,15 +145,12 @@ public class ProvMnSController implements ProvMnS { @Override public ResponseEntity deleteMoi(final HttpServletRequest httpServletRequest) { - final RequestParameters requestParameters = parameterMapper.extractRequestParameters(httpServletRequest); + final RequestParameters requestParameters = ParameterHelper.extractRequestParameters(httpServletRequest); try { final YangModelCmHandle yangModelCmHandle = getAndValidateYangModelCmHandle(requestParameters); - final DeleteOperationDetails deleteOperationDetails = - operationDetailsFactory.buildDeleteOperationDetails(requestParameters.toTargetFdn()); - checkPermission(yangModelCmHandle, requestParameters.toTargetFdn(), deleteOperationDetails); - final String targetFdn = requestParameters.toTargetFdn(); + checkPermission(yangModelCmHandle, requestParameters.fdn(), DELETE_OPERATION_DETAILS); final UrlTemplateParameters urlTemplateParameters = - parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, targetFdn); + parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestParameters.fdn()); return dmiRestClient.synchronousDeleteOperation(DATA, urlTemplateParameters); } catch (final Exception exception) { throw toProvMnSException(httpServletRequest.getMethod(), exception, NO_OP); @@ -167,46 +159,49 @@ public class ProvMnSController implements ProvMnS { private YangModelCmHandle getAndValidateYangModelCmHandle(final RequestParameters requestParameters) throws ProvMnSException { - final String alternateId = requestParameters.toTargetFdn(); + final String fdn = requestParameters.fdn(); try { - final String cmHandleId = alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(alternateId, "/"); + final String cmHandleId = alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(fdn, "/"); final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(cmHandleId); if (!StringUtils.hasText(yangModelCmHandle.getDataProducerIdentifier())) { - throw new ProvMnSException(requestParameters.getHttpMethodName(), UNPROCESSABLE_ENTITY, + throw new ProvMnSException(requestParameters.httpMethodName(), UNPROCESSABLE_ENTITY, PROVMNS_NOT_SUPPORTED_ERROR_MESSAGE, NO_OP); } if (yangModelCmHandle.getCompositeState().getCmHandleState() != CmHandleState.READY) { final String title = yangModelCmHandle.getId() + " is not in READY state. Current state: " + yangModelCmHandle.getCompositeState().getCmHandleState().name(); - throw new ProvMnSException(requestParameters.getHttpMethodName(), NOT_ACCEPTABLE, title, NO_OP); + throw new ProvMnSException(requestParameters.httpMethodName(), NOT_ACCEPTABLE, title, NO_OP); } return yangModelCmHandle; } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) { - final String title = alternateId + " not found"; - throw new ProvMnSException(requestParameters.getHttpMethodName(), NOT_FOUND, title, NO_OP); + throw new ProvMnSException(requestParameters.httpMethodName(), NOT_FOUND, fdn + " not found", NO_OP); } } private void checkPermission(final YangModelCmHandle yangModelCmHandle, - final String alternateId, + final String resourceIdentifier, final OperationDetails operationDetails) { final OperationType operationType = OperationType.fromOperationName(operationDetails.operation()); final String operationDetailsAsJson = jsonObjectMapper.asJsonString(operationDetails); - policyExecutor.checkPermission(yangModelCmHandle, operationType, NO_AUTHORIZATION, alternateId, + policyExecutor.checkPermission(yangModelCmHandle, operationType, NO_AUTHORIZATION, resourceIdentifier, operationDetailsAsJson); } - private void checkPermissionForEachPatchItem(final RequestParameters requestParameters, + private void checkPermissionForEachPatchItem(final String baseFdn, final List patchItems, final YangModelCmHandle yangModelCmHandle) { + int patchItemCounter = 0; for (final PatchItem patchItem : patchItems) { + final String extendedPath = baseFdn + patchItem.getPath(); + final RequestParameters requestParameters = ParameterHelper.createRequestParametersForPatch(extendedPath); final OperationDetails operationDetails = operationDetailsFactory.buildOperationDetails(requestParameters, patchItem); try { - checkPermission(yangModelCmHandle, requestParameters.toTargetFdn(), operationDetails); + checkPermission(yangModelCmHandle, requestParameters.fdn(), operationDetails); + patchItemCounter++; } catch (final Exception exception) { final String httpMethodName = "PATCH"; - throw toProvMnSException(httpMethodName, exception, patchItem.getOp().getValue()); + throw toProvMnSException(httpMethodName, exception, "/" + patchItemCounter); } } } diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSRestExceptionHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSRestExceptionHandler.java index 0f85917589..e6e9bcdd04 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSRestExceptionHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSRestExceptionHandler.java @@ -19,7 +19,7 @@ package org.onap.cps.ncmp.rest.controller; -import static org.onap.cps.ncmp.impl.provmns.ParameterMapper.NO_OP; +import static org.onap.cps.ncmp.impl.provmns.ParameterHelper.NO_OP; import java.util.Map; import java.util.Objects; 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 3c30932143..f2edc9c1ed 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,6 +22,8 @@ package org.onap.cps.ncmp.rest.controller import com.fasterxml.jackson.databind.ObjectMapper import io.netty.handler.timeout.TimeoutException +import org.onap.cps.api.exceptions.DataValidationException +import org.onap.cps.ncmp.api.data.models.OperationType import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException import org.onap.cps.ncmp.api.inventory.models.CompositeState import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException @@ -30,7 +32,6 @@ 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.ParameterMapper import org.onap.cps.ncmp.impl.provmns.ParametersBuilder import org.onap.cps.ncmp.impl.provmns.model.PatchItem import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher @@ -55,7 +56,6 @@ import static org.springframework.http.HttpStatus.NOT_ACCEPTABLE import static org.springframework.http.HttpStatus.NOT_FOUND import static org.springframework.http.HttpStatus.OK import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE -import static org.springframework.http.HttpStatus.SEE_OTHER import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY import static org.springframework.http.HttpStatus.UNSUPPORTED_MEDIA_TYPE import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete @@ -81,9 +81,6 @@ class ProvMnSControllerSpec extends Specification { @Autowired OperationDetailsFactory operationDetailsFactory - @SpringBean - ParameterMapper parameterMapper = new ParameterMapper() - @SpringBean PolicyExecutor mockPolicyExecutor = Mock() @@ -94,18 +91,20 @@ class ProvMnSControllerSpec extends Specification { ObjectMapper objectMapper = new ObjectMapper() @SpringBean - JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(objectMapper) + JsonObjectMapper spiedJsonObjectMapper = Spy(new JsonObjectMapper(objectMapper)) - static def resourceAsJson = '{"id":"test"}' + static def resourceAsJson = '{"id":"test", "objectClass": "Test", "attributes": { "attr1": "value1"} }' static def validCmHandle = new YangModelCmHandle(id:'ch-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: READY)) static def cmHandleWithoutDataProducer = new YangModelCmHandle(id:'ch-1', dmiServiceName: 'someDmiService', compositeState: new CompositeState(cmHandleState: READY)) static def cmHandleNotReady = new YangModelCmHandle(id:'ch-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: ADVISED)) static def patchMediaType = new MediaType('application', 'json-patch+json') static def patchMediaType3gpp = new MediaType('application', '3gpp-json-patch+json') - static def patchJsonBody = '[{"op":"replace","path":"className/attributes/attr1","value":{"id":"test"}}]' - static def patchJsonBody3gpp = '[{"op":"replace","path":"className#/attributes/attr1","value":{"id":"test"}}]' - static def patchJsonBodyInvalid = '[{"op":"replace","path":"/test","value":"INVALID"}]' + static def patchJsonBody = '[{"op":"replace","path":"/child=id2/attributes","value":{"attr1":"test"}}]' + static def patchJsonBody3gpp = '[{"op":"replace","path":"/child=id2#/attributes/attr1","value":"test"}]' + static def patchJsonBodyInvalid = '[{"op":"replace","path":"/test","value":{}}]' + + static def expectedDeleteOperationDetails = '{"operation":"delete","targetIdentifier":"","changeRequest":{}}' @Value('${rest.api.provmns-base-path}') def provMnSBasePath @@ -114,7 +113,6 @@ class ProvMnSControllerSpec extends Specification { int maxNumberOfPatchOperations static def NO_CONTENT = '' - static def STATUS_NOT_RELEVANT = SEE_OTHER def 'Get resource data #scenario.'() { given: 'resource data url' @@ -180,7 +178,6 @@ class ProvMnSControllerSpec extends Specification { new Exception("my message") || INTERNAL_SERVER_ERROR | 'APPLICATION_LAYER_ERROR' | 'my message' } - def 'Get resource data request with invalid URL: #scenario.'() { given: 'resource data url' def getUrl = "$provMnSBasePath/$version/$fdn$queryParameter" @@ -228,6 +225,10 @@ class ProvMnSControllerSpec extends Specification { def provmnsUrl = "$provMnSBasePath/v1/myClass=id1" and: 'alternate Id can be matched' mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1' + and: 'resource id for policy executor points to child node' + def expectedResourceIdForPolicyExecutor = '/myClass=id1/child=id2' + and: 'operation details has correct class and attributes, target identifier points to parent' + def expectedOperationDetails = '{"operation":"update","targetIdentifier":"/myClass=id1","changeRequest":{"child":[{"id":"id2","attributes":{"attr1":"test"}}]}}' and: 'persistence service returns yangModelCmHandle' mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle and: 'dmi provides a response' @@ -238,16 +239,74 @@ class ProvMnSControllerSpec extends Specification { .content(jsonBody)) .andReturn().response then: 'response status is the same as what DMI gave' - assert response.status == expectedRespinsStatusFromProvMnS.value() + assert response.status == expectedResponseStatusFromProvMnS.value() and: 'the response contains the expected content' - assert response.contentAsString.contains(expectedResponseContent) + assert response.contentAsString.contains('content from DMI') + and: 'policy executor was invoked with the expected parameters' + 1 * mockPolicyExecutor.checkPermission(_, OperationType.UPDATE, _, expectedResourceIdForPolicyExecutor, expectedOperationDetails) where: 'following scenarios are applied' - scenario | contentMediaType | jsonBody | responseStatusFromDmi || expectedResponseContent | expectedRespinsStatusFromProvMnS - 'happy flow 3gpp' | patchMediaType3gpp | patchJsonBody3gpp | OK || 'content from DMI' | OK - 'happy flow' | patchMediaType | patchJsonBody | OK || 'content from DMI' | OK - 'error from DMI' | patchMediaType | patchJsonBody | I_AM_A_TEAPOT || 'content from DMI' | I_AM_A_TEAPOT - 'invalid Json' | patchMediaType | patchJsonBodyInvalid | STATUS_NOT_RELEVANT || '"type":"VALIDATION_ERROR"' | BAD_REQUEST - 'malformed Json' | patchMediaType | '{malformed]' | STATUS_NOT_RELEVANT || NO_CONTENT | BAD_REQUEST + scenario | contentMediaType | jsonBody | responseStatusFromDmi || expectedResponseStatusFromProvMnS + 'happy flow 3gpp' | patchMediaType3gpp | patchJsonBody3gpp | OK || OK + 'happy flow' | patchMediaType | patchJsonBody | OK || OK + 'error from DMI' | patchMediaType | patchJsonBody | I_AM_A_TEAPOT || I_AM_A_TEAPOT + } + + def 'Attempt Patch request with malformed json.'() { + given: 'provmns url' + def provmnsUrl = "$provMnSBasePath/v1/myClass=id1" + and: 'alternate Id can be matched' + mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1' + and: 'persistence service returns yangModelCmHandle' + mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle + when: 'patch request is performed' + def response = mvc.perform(patch(provmnsUrl) + .contentType(patchMediaType) + .content('{malformed}')) + .andReturn().response + then: 'response status is Bad Request' + assert response.status == BAD_REQUEST.value() + and: 'the response content is empty' + assert response.contentAsString.isEmpty() + } + + def 'Attempt Patch request with json exception during processing.'() { + given: 'provmns url' + def provmnsUrl = "$provMnSBasePath/v1/myClass=id1" + and: 'alternate Id can be matched' + mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1' + and: 'persistence service returns yangModelCmHandle' + mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle + and: + spiedJsonObjectMapper.asJsonString(_) >> { throw new DataValidationException('my message','some details') } + when: 'patch request is performed' + def response = mvc.perform(patch(provmnsUrl) + .contentType(patchMediaType) + .content(patchJsonBody)) + .andReturn().response + then: 'response status is Bad Request' + assert response.status == BAD_REQUEST.value() + and: 'the response contains the correct type and original exception message' + assert response.contentAsString.contains('"type":"VALIDATION_ERROR"') + assert response.contentAsString.contains('my message') + } + + def 'Patch remove request.'() { + given: 'resource data url' + def url = "$provMnSBasePath/v1/myClass=id1" + and: 'alternate Id can be matched' + mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1' + and: 'persistence service returns valid yangModelCmHandle' + mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle + def expectedResourceIdentifier = '/myClass=id1/childClass=1/grandchildClass=1' + when: 'patch data resource request is performed' + def response = mvc.perform(patch(url) + .contentType(patchMediaType) + .content('[{"op":"remove","path":"/childClass=1/grandchildClass=1"}]')) + .andReturn().response + then: 'response status is OK' + assert response.status == OK.value() + and: 'Policy Executor was invoked with correct details' + 1 * mockPolicyExecutor.checkPermission(_, OperationType.DELETE, _, expectedResourceIdentifier, expectedDeleteOperationDetails) } def 'Patch request with no permission from Coordination Management (aka Policy Executor).'() { @@ -268,8 +327,8 @@ class ProvMnSControllerSpec extends Specification { assert response.status == CONFLICT.value() and: 'response contains the correct type' assert response.contentAsString.contains('"type":"APPLICATION_LAYER_ERROR"') - and: 'response contains the bad operation' - assert response.contentAsString.contains('"badOp":"replace"') + and: 'response contains the bad operation index' + assert response.contentAsString.contains('"badOp":"/0"') and: 'response contains the message from Policy Executor (as title)' assert response.contentAsString.contains('"title":"denied for test"') } @@ -299,7 +358,7 @@ class ProvMnSControllerSpec extends Specification { for (def i = 0; i <= maxNumberOfPatchOperations; i++) { patchItems.add(new PatchItem(op: 'REMOVE', path: 'somePath')) } - def patchItemsJsonRequestBody = jsonObjectMapper.asJsonString(patchItems) + def patchItemsJsonRequestBody = spiedJsonObjectMapper.asJsonString(patchItems) when: 'patch data resource request is performed' def response = mvc.perform(patch(url) .contentType(patchMediaType) @@ -315,13 +374,17 @@ class ProvMnSControllerSpec extends Specification { def 'Put resource data request with #scenario.'() { given: 'resource data url' - def putUrl = "$provMnSBasePath/v1/myClass=id1" + def putUrl = "$provMnSBasePath/v1/myClass=id1/childClass=1/grandChildClass=2" and: 'alternate Id can be matched' - mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1' + mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1/childClass=1/grandChildClass=2', "/") >> 'ch-1' and: 'persistence service returns yangModelCmHandle' mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle and: 'dmi provides a response' mockDmiRestClient.synchronousPutOperation(*_) >> new ResponseEntity<>(responseContentFromDmi, responseStatusFromDmi) + and: 'The expected resource identifier for policy executor is the FDN to grandchild' + def expectedResourceIdentifier = '/myClass=id1/childClass=1/grandChildClass=2' + and: 'The change request target identifier is the FDN to parent and last class as object name in change request' + def expectedChangeRequest = '{"operation":"create","targetIdentifier":"/myClass=id1/childClass=1","changeRequest":{"grandChildClass":[{"id":"2","attributes":{"attr1":"value1"}}]}}' when: 'put data resource request is performed' def response = mvc.perform(put(putUrl) .contentType(MediaType.APPLICATION_JSON) @@ -331,6 +394,8 @@ class ProvMnSControllerSpec extends Specification { assert response.status == responseStatusFromDmi.value() and: 'the content is whatever the DMI returned' assert response.contentAsString == responseContentFromDmi + and: 'The policy executor was invoked with the expected parameters' + 1 * mockPolicyExecutor.checkPermission(_, OperationType.CREATE, _, expectedResourceIdentifier, expectedChangeRequest) where: 'following responses returned by DMI' scenario | responseStatusFromDmi | responseContentFromDmi 'happy flow' | OK | 'content from DMI' @@ -355,9 +420,9 @@ class ProvMnSControllerSpec extends Specification { def 'Delete resource data request with #scenario.'() { given: 'resource data url' - def deleteUrl = "$provMnSBasePath/v1/myClass=id1" + def deleteUrl = "$provMnSBasePath/v1/myClass=id1/childClass=1/grandChildClass=2" and: 'alternate Id can be matched' - mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1', "/") >> 'ch-1' + mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/myClass=id1/childClass=1/grandChildClass=2', "/") >> 'ch-1' and: 'persistence service returns yangModelCmHandle' mockInventoryPersistence.getYangModelCmHandle('ch-1') >> validCmHandle and: 'dmi provides a response' @@ -368,6 +433,8 @@ class ProvMnSControllerSpec extends Specification { assert response.status == responseStatusFromDmi.value() and: 'the content is whatever the DMI returned' assert response.contentAsString == responseContentFromDmi + and: 'Policy Executor was invoked with correct resource identifier and almost empty operation details (not used for delete!)' + 1 * mockPolicyExecutor.checkPermission(_, OperationType.DELETE, _, '/myClass=id1/childClass=1/grandChildClass=2', expectedDeleteOperationDetails) where: 'following responses returned by DMI' scenario | responseStatusFromDmi | responseContentFromDmi 'happy flow' | OK | 'content from DMI' diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/CreateOperationDetails.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/CreateOperationDetails.java deleted file mode 100644 index 8f85eaedc3..0000000000 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/CreateOperationDetails.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2025 OpenInfra Foundation Europe - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ - -package org.onap.cps.ncmp.impl.data.policyexecutor; - -import com.fasterxml.jackson.annotation.JsonInclude; -import java.util.List; -import java.util.Map; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record CreateOperationDetails(String operation, - String targetIdentifier, - Map> changeRequest) implements OperationDetails {} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/DeleteOperationDetails.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/DeleteOperationDetails.java deleted file mode 100644 index 62547b59fb..0000000000 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/DeleteOperationDetails.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2025 OpenInfra Foundation Europe - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ - -package org.onap.cps.ncmp.impl.data.policyexecutor; - -import com.fasterxml.jackson.annotation.JsonInclude; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record DeleteOperationDetails(String operation, - String targetIdentifier) implements OperationDetails {} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetails.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetails.java index bb0a3faf14..7c64283e2a 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/OperationDetails.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2025 OpenInfra Foundation Europe + * Copyright (C) 2025-2026 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. @@ -20,6 +20,11 @@ package org.onap.cps.ncmp.impl.data.policyexecutor; -public interface OperationDetails { - String operation(); -} +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record OperationDetails(String operation, + String targetIdentifier, + Map> changeRequest) {} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java index 614d95dba4..2ad53cbe7d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactory.java @@ -20,11 +20,10 @@ package org.onap.cps.ncmp.impl.data.policyexecutor; +import static java.util.Collections.emptyMap; import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE; -import static org.onap.cps.ncmp.api.data.models.OperationType.DELETE; import static org.onap.cps.ncmp.api.data.models.OperationType.UPDATE; -import com.google.common.base.Strings; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -32,6 +31,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.api.data.models.OperationType; import org.onap.cps.ncmp.api.exceptions.ProvMnSException; +import org.onap.cps.ncmp.impl.provmns.ParameterHelper; import org.onap.cps.ncmp.impl.provmns.RequestParameters; import org.onap.cps.ncmp.impl.provmns.model.PatchItem; import org.onap.cps.utils.JsonObjectMapper; @@ -43,6 +43,8 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class OperationDetailsFactory { + public static final OperationDetails DELETE_OPERATION_DETAILS = new OperationDetails("delete", "", emptyMap()); + private static final String ATTRIBUTE_NAME_SEPARATOR = "/"; private static final String REGEX_FOR_LEADING_AND_TRAILING_SEPARATORS = "(^/)|(/$)"; @@ -51,8 +53,8 @@ public class OperationDetailsFactory { /** * Create OperationDetails object from ProvMnS request details. * - * @param requestParameters request parameters including uri-ldn-first-part, className and id - * @param patchItem provided request payload + * @param requestParameters request parameters including uri-ldn-first-part, className and id + * @param patchItem provided request payload * @return OperationDetails object */ public OperationDetails buildOperationDetails(final RequestParameters requestParameters, @@ -60,17 +62,17 @@ public class OperationDetailsFactory { final OperationDetails operationDetails; switch (patchItem.getOp()) { case ADD: - operationDetails = buildCreateOperationDetails(CREATE, requestParameters, patchItem.getValue()); + operationDetails = buildOperationDetailsForPatchItem(CREATE, requestParameters, patchItem); break; case REPLACE: if (patchItem.getPath().contains("#/attributes")) { - operationDetails = buildCreateOperationDetailsForUpdateWithHash(requestParameters, patchItem); + operationDetails = buildOperationDetailsForPatchItemWithHash(requestParameters, patchItem); } else { - operationDetails = buildCreateOperationDetails(UPDATE, requestParameters, patchItem.getValue()); + operationDetails = buildOperationDetailsForPatchItem(UPDATE, requestParameters, patchItem); } break; case REMOVE: - operationDetails = buildDeleteOperationDetails(requestParameters.toTargetFdn()); + operationDetails = DELETE_OPERATION_DETAILS; break; default: throw new ProvMnSException("PATCH", HttpStatus.UNPROCESSABLE_ENTITY, @@ -80,33 +82,51 @@ public class OperationDetailsFactory { } /** - * Build a CreateOperationDetails object from ProvMnS request details. + * Build a OperationDetails object from ProvMnS request details. * - * @param operationType Type of operation create, update. - * @param requestParameters request parameters including uri-ldn-first-part, className and id - * @param resourceAsObject provided request payload - * @return CreateOperationDetails object + * @param operationType Type of operation create, update. + * @param requestParameters request parameters including uri-ldn-first-part, className and id + * @param resourceAsObject provided request payload + * @return OperationDetails object */ - public CreateOperationDetails buildCreateOperationDetails(final OperationType operationType, - final RequestParameters requestParameters, - final Object resourceAsObject) { + public OperationDetails buildOperationDetails(final OperationType operationType, + final RequestParameters requestParameters, + final Object resourceAsObject) { final ResourceObjectDetails resourceObjectDetails = createResourceObjectDetails(resourceAsObject, requestParameters); final OperationEntry operationEntry = new OperationEntry(resourceObjectDetails.id(), resourceObjectDetails.attributes()); - return new CreateOperationDetails(operationType.name(), - requestParameters.getUriLdnFirstPart(), - Map.of(resourceObjectDetails.objectClass(), List.of(operationEntry))); + final Map> changeRequestAsMap = + Map.of(resourceObjectDetails.objectClass(), List.of(operationEntry)); + final String targetIdentifier = ParameterHelper.extractParentFdn(requestParameters.fdn()); + return new OperationDetails(operationType.getOperationName(), targetIdentifier, changeRequestAsMap); } /** - * Builds a DeleteOperationDetails object from provided alternate id. + * Build OperationDetails for a specific patch item. * - * @param alternateId alternate id for request - * @return DeleteOperationDetails object + * @param operationType the type of operation (CREATE, UPDATE) + * @param requestParameters request parameters including uri-ldn-first-part, className and id + * @param patchItem the patch item containing operation details + * @return OperationDetails object for the patch item */ - public DeleteOperationDetails buildDeleteOperationDetails(final String alternateId) { - return new DeleteOperationDetails(DELETE.name(), alternateId); + public OperationDetails buildOperationDetailsForPatchItem(final OperationType operationType, + final RequestParameters requestParameters, + final PatchItem patchItem) { + final Map resourceAsObject = new HashMap<>(2); + resourceAsObject.put("id", requestParameters.id()); + resourceAsObject.put("attributes", patchItem.getValue()); + return buildOperationDetails(operationType, requestParameters, resourceAsObject); + } + + private OperationDetails buildOperationDetailsForPatchItemWithHash(final RequestParameters requestParameters, + final PatchItem patchItem) { + final Map attributeHierarchyAsMap = createNestedMap(patchItem); + final OperationEntry operationEntry = new OperationEntry(requestParameters.id(), attributeHierarchyAsMap); + final String targetIdentifier = ParameterHelper.extractParentFdn(requestParameters.fdn()); + final Map> operationEntriesPerObjectClass = new HashMap<>(); + operationEntriesPerObjectClass.put(requestParameters.className(), List.of(operationEntry)); + return new OperationDetails(UPDATE.getOperationName(), targetIdentifier, operationEntriesPerObjectClass); } @SuppressWarnings("unchecked") @@ -114,31 +134,9 @@ public class OperationDetailsFactory { final RequestParameters requestParameters) { final String resourceAsJson = jsonObjectMapper.asJsonString(resourceAsObject); final Map resourceAsMap = jsonObjectMapper.convertJsonString(resourceAsJson, Map.class); - return new ResourceObjectDetails(requestParameters.getId(), - extractObjectClass(resourceAsMap, requestParameters), + return new ResourceObjectDetails(requestParameters.id(), + requestParameters.className(), resourceAsMap.get("attributes")); - - } - - private static String extractObjectClass(final Map resourceAsMap, - final RequestParameters requestParameters) { - final String objectClass = (String) resourceAsMap.get("objectClass"); - if (Strings.isNullOrEmpty(objectClass)) { - return requestParameters.getClassName(); - } - return objectClass; - } - - private CreateOperationDetails buildCreateOperationDetailsForUpdateWithHash( - final RequestParameters requestParameters, - final PatchItem patchItem) { - final Map> operationEntriesPerObjectClass = new HashMap<>(); - final String className = requestParameters.getClassName(); - final Map attributeHierarchyAsMap = createNestedMap(patchItem); - final OperationEntry operationEntry = new OperationEntry(requestParameters.getId(), attributeHierarchyAsMap); - operationEntriesPerObjectClass.put(className, List.of(operationEntry)); - return new CreateOperationDetails(UPDATE.getOperationName(), requestParameters.getUriLdnFirstPart(), - operationEntriesPerObjectClass); } private Map createNestedMap(final PatchItem patchItem) { 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 96199f94b5..d94a69f064 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 @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved. + * Copyright (C) 2021-2026 OpenInfra Foundation Europe. All rights reserved. * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -166,9 +166,9 @@ public class DmiRestClient { * @return ResponseEntity containing the response from the DMI. */ public ResponseEntity synchronousPatchOperation(final RequiredDmiService requiredDmiService, - final Object body, - final UrlTemplateParameters urlTemplateParameters, - final String contentType) { + final Object body, + final UrlTemplateParameters urlTemplateParameters, + final String contentType) { return getWebClient(requiredDmiService) .patch() .uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables()) diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterHelper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterHelper.java new file mode 100644 index 0000000000..d2903858a3 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterHelper.java @@ -0,0 +1,121 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025-2026 OpenInfra Foundation Europe + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.impl.provmns; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.NoArgsConstructor; +import org.onap.cps.ncmp.api.exceptions.ProvMnSException; +import org.springframework.http.HttpStatus; + +@NoArgsConstructor +public class ParameterHelper { + + public static final String NO_OP = null; + private static final String PROVMNS_BASE_PATH = "ProvMnS/v\\d+/"; + private static final String INVALID_PATH_DETAILS_TEMPLATE = "%s not a valid path"; + private static final int PATH_VARIABLES_EXPECTED_LENGTH = 2; + private static final int REQUEST_FDN_INDEX = 1; + + /** + * Converts HttpServletRequest to RequestParameters. + * + * @param httpServletRequest HttpServletRequest object containing the path + * @return RequestParameters object containing http method and FDN parameters + */ + public static RequestParameters extractRequestParameters(final HttpServletRequest httpServletRequest) { + final String uriPath = (String) httpServletRequest.getAttribute( + "org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping"); + final String[] pathVariables = uriPath.split(PROVMNS_BASE_PATH); + if (pathVariables.length != PATH_VARIABLES_EXPECTED_LENGTH) { + throw createProvMnSException(httpServletRequest.getMethod(), uriPath); + } + final String fdn = "/" + pathVariables[REQUEST_FDN_INDEX]; + return createRequestParameters(httpServletRequest.getMethod(), fdn); + } + + /** + * Create RequestParameters object for PATCH operations. + * + * @param pathWithAttributes the path a fdn possibly with containing attributes + * @return RequestParameters object for PATCH operation + */ + public static RequestParameters createRequestParametersForPatch(final String pathWithAttributes) { + final String fdn = removeTrailingHash(extractFdn(pathWithAttributes)); + return createRequestParameters("PATCH", fdn); + } + + /** + * Extract parent FDN from the given path allowing only className=id pairs. + * + * @param path the path to convert + * @return parent FDN + */ + public static String extractParentFdn(final String path) { + return extractFdn(path, 2); + } + + /** + * Extract FDN from the given path allowing only className=id pairs. + * + * @param path the path to convert + * @return FDN + */ + public static String extractFdn(final String path) { + return extractFdn(path, 1); + } + + private static String extractFdn(final String path, final int indexFromEnd) { + final String[] segments = path.split("/"); + int count = 0; + for (int i = segments.length - 1; i >= 0; i--) { + if (segments[i].contains("=") && ++count == indexFromEnd) { + return String.join("/", java.util.Arrays.copyOfRange(segments, 0, i + 1)); + } + } + return ""; + } + + private static String removeTrailingHash(final String string) { + return string.endsWith("#") ? string.substring(0, string.length() - 1) : string; + } + + private static RequestParameters createRequestParameters(final String httpMethodName, + final String fdn) { + final int lastSlashIndex = fdn.lastIndexOf('/'); + final String classNameAndId; + final String uriLdnFirstPart; + uriLdnFirstPart = fdn.substring(0, lastSlashIndex); + classNameAndId = fdn.substring(lastSlashIndex + 1); + final String[] splitClassNameId = classNameAndId.split("=", 2); + if (splitClassNameId.length != 2) { + throw createProvMnSException(httpMethodName, fdn); + } + final String className = splitClassNameId[0]; + final String id = removeTrailingHash(splitClassNameId[1]); + return new RequestParameters(httpMethodName, fdn, uriLdnFirstPart, className, id); + } + + private static ProvMnSException createProvMnSException(final String httpMethodName, final String uriPath) { + final String title = String.format(INVALID_PATH_DETAILS_TEMPLATE, uriPath); + return new ProvMnSException(httpMethodName, HttpStatus.UNPROCESSABLE_ENTITY, title, NO_OP); + } + +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java deleted file mode 100644 index 3b6f085d62..0000000000 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2025 OpenInfra Foundation Europe - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ - -package org.onap.cps.ncmp.impl.provmns; - -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import org.onap.cps.ncmp.api.exceptions.ProvMnSException; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class ParameterMapper { - - public static final String NO_OP = null; - private static final String PROVMNS_BASE_PATH = "ProvMnS/v\\d+/"; - private static final String INVALID_PATH_DETAILS_TEMPLATE = "%s not a valid path"; - private static final int PATH_VARIABLES_EXPECTED_LENGTH = 2; - private static final int OBJECT_INSTANCE_INDEX = 1; - - /** - * Converts HttpServletRequest to RequestParameters. - * - * @param httpServletRequest HttpServletRequest object containing the path - * @return RequestParameters object containing http method and parsed parameters - */ - public RequestParameters extractRequestParameters(final HttpServletRequest httpServletRequest) { - final String uriPath = (String) httpServletRequest.getAttribute( - "org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping"); - final String[] pathVariables = uriPath.split(PROVMNS_BASE_PATH); - if (pathVariables.length != PATH_VARIABLES_EXPECTED_LENGTH) { - throw createProvMnSException(httpServletRequest.getMethod(), uriPath); - } - final int lastSlashIndex = pathVariables[1].lastIndexOf('/'); - final RequestParameters requestParameters = new RequestParameters(); - requestParameters.setHttpMethodName(httpServletRequest.getMethod()); - final String classNameAndId; - if (lastSlashIndex < 0) { - requestParameters.setUriLdnFirstPart(""); - classNameAndId = pathVariables[OBJECT_INSTANCE_INDEX]; - } else { - final String uriLdnFirstPart = "/" + pathVariables[OBJECT_INSTANCE_INDEX].substring(0, lastSlashIndex); - requestParameters.setUriLdnFirstPart(uriLdnFirstPart); - classNameAndId = pathVariables[OBJECT_INSTANCE_INDEX].substring(lastSlashIndex + 1); - } - final String[] splitClassNameId = classNameAndId.split("=", 2); - if (splitClassNameId.length != 2) { - throw createProvMnSException(httpServletRequest.getMethod(), uriPath); - } - requestParameters.setClassName(splitClassNameId[0]); - requestParameters.setId(splitClassNameId[1]); - return requestParameters; - } - - private ProvMnSException createProvMnSException(final String httpMethodName, final String uriPath) { - final String title = String.format(INVALID_PATH_DETAILS_TEMPLATE, uriPath); - return new ProvMnSException(httpMethodName, HttpStatus.UNPROCESSABLE_ENTITY, title, NO_OP); - } - -} 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 dc6a2597cc..18e7965595 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 @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2025 OpenInfra Foundation Europe + * Copyright (C) 2025-2026 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. @@ -38,17 +38,17 @@ public class ParametersBuilder { /** * Creates a UrlTemplateParameters object containing the relevant fields for read requests. * - * @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID - * @param targetFdn Target FDN for the resource - * @param scope Provided className parameter - * @param filter Filter string - * @param attributes Attributes List - * @param fields Fields list - * @param dataNodeSelector dataNodeSelector parameter + * @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID + * @param resourceIdentifier Target FDN for the resource + * @param scope Provided className parameter + * @param filter Filter string + * @param attributes Attributes List + * @param fields Fields list + * @param dataNodeSelector dataNodeSelector parameter * @return UrlTemplateParameters object. */ public UrlTemplateParameters createUrlTemplateParametersForRead(final YangModelCmHandle yangModelCmHandle, - final String targetFdn, + final String resourceIdentifier, final Scope scope, final String filter, final List attributes, @@ -56,9 +56,9 @@ public class ParametersBuilder { final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector) { final String dmiServiceName = yangModelCmHandle.resolveDmiServiceName(DATA); - final String targetFdnWithoutPrecedingSlash = targetFdn.substring(1); + final String resourceIdentifierWithoutPrecedingSlash = resourceIdentifier.substring(1); return RestServiceUrlTemplateBuilder.newInstance() - .fixedPathSegment(targetFdnWithoutPrecedingSlash) + .fixedPathSegment(resourceIdentifierWithoutPrecedingSlash) .queryParameter("scopeType", scope.getScopeType() != null ? scope.getScopeType().getValue() : null) .queryParameter("scopeLevel", scope.getScopeLevel() != null ? scope.getScopeLevel().toString() : null) .queryParameter("filter", filter) diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/RequestParameters.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/RequestParameters.java index a85bb249b4..fb823d42a8 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/RequestParameters.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/RequestParameters.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2025 OpenInfra Foundation Europe + * Copyright (C) 2025-2026 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. @@ -20,24 +20,9 @@ package org.onap.cps.ncmp.impl.provmns; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class RequestParameters { - - private String httpMethodName; - private String uriLdnFirstPart; - private String className; - private String id; - - /** - * Gets target FDN by combining URI-LDN-First-Part, className and id. - * - * @return String of FDN - */ - public String toTargetFdn() { - return uriLdnFirstPart + "/" + className + "=" + id; - } -} +public record RequestParameters( + String httpMethodName, + String fdn, + String uriLdnFirstPart, + String className, + String id) {} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactorySpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactorySpec.groovy index ab4abea689..1c41b4293c 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactorySpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetailsFactorySpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2024-2025 OpenInfra Foundation Europe. All rights reserved. + * Copyright (C) 2024-2026 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. @@ -20,7 +20,6 @@ package org.onap.cps.ncmp.impl.data.policyexecutor - import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.ncmp.api.exceptions.ProvMnSException import org.onap.cps.ncmp.impl.provmns.RequestParameters @@ -36,58 +35,44 @@ import static org.onap.cps.ncmp.api.data.models.OperationType.UPDATE class OperationDetailsFactorySpec extends Specification { def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) + def requestPathParameters = new RequestParameters('some method', '/parent=1/child=2','some uri', 'class in uri', 'id in uri') OperationDetailsFactory objectUnderTest = new OperationDetailsFactory(jsonObjectMapper) - static def complexValueAsResource = new ResourceOneOf(id: 'myId', attributes: ['myAttribute1:myValue1', 'myAttribute2:myValue2'], objectClass: 'myClassName') - static def simpleValueAsResource = new ResourceOneOf(id: 'myId', attributes: ['simpleAttribute:1'], objectClass: 'myClassName') - def 'Build create operation details with all properties.'() { - given: 'request parameters and resource' - def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'my uri', className: 'class in uri', id: 'my id') - def resource = new ResourceOneOf(id: 'some resource id', objectClass: 'class in resource') + given: 'a resource' + def resource = new ResourceOneOf(id: 'id in resource', objectClass: 'class in resource') when: 'create operation details are built' - def result = objectUnderTest.buildCreateOperationDetails(CREATE, requestPathParameters, resource) + def result = objectUnderTest.buildOperationDetails(CREATE, requestPathParameters, resource) then: 'all details are correct' - assert result.targetIdentifier == 'my uri' - assert result.changeRequest.keySet()[0] == 'class in resource' - assert result.changeRequest['class in resource'][0].id == 'my id' + assert result.targetIdentifier == '/parent=1' + assert result.changeRequest.keySet()[0] == 'class in uri' + assert result.changeRequest['class in uri'][0].id == 'id in uri' } - def 'Build replace operation details with all properties where class name in body is #scenario.'() { - given: 'request parameters and resource' - def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'my uri', className: 'class in uri', id: 'some id') + def 'Build replace (~create) operation details with all properties where class name in body is #scenario.'() { + given: 'a resource' def resource = new ResourceOneOf(id: 'some resource id', objectClass: classNameInBody) when: 'replace operation details are built' - def result = objectUnderTest.buildCreateOperationDetails(CREATE, requestPathParameters, resource) + def result = objectUnderTest.buildOperationDetails(CREATE, requestPathParameters, resource) then: 'all details are correct' - assert result.targetIdentifier == 'my uri' - assert result.changeRequest.keySet()[0] == expectedChangeRequestKey + assert result.targetIdentifier == '/parent=1' + assert result.changeRequest.keySet()[0] == 'class in uri' where: - scenario | classNameInBody || expectedChangeRequestKey - 'populated' | 'class in body' || 'class in body' - 'empty' | '' || 'class in uri' - 'null' | null || 'class in uri' - } - - def 'Build delete operation details with all properties'() { - given: 'request parameters' - def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'my uri', className: 'classNameInUri', id: 'myId') - when: 'delete operation details are built' - def result = objectUnderTest.buildDeleteOperationDetails(requestPathParameters.toTargetFdn()) - then: 'all details are correct' - assert result.targetIdentifier == 'my uri/classNameInUri=myId' + scenario | classNameInBody + 'populated' | 'class in body' + 'empty' | '' + 'null' | null } def 'Single patch operation with #patchOperationType checks correct operation type.'() { - given: 'request parameters and single patch item' - def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'some uri', className: 'some class') + given: 'a resource and single patch item' def resource = new ResourceOneOf(id: 'some resource id') def patchItem = new PatchItem(op: patchOperationType, 'path':'some uri', value: resource) when: 'operation details is created' def result = objectUnderTest.buildOperationDetails(requestPathParameters, patchItem) then: 'it has the correct operation type (for Policy Executor check)' - assert result.operation() == expectedPolicyExecutorOperationType.name() + assert result.operation() == expectedPolicyExecutorOperationType.operationName where: 'following operations are used' patchOperationType | expectedPolicyExecutorOperationType 'ADD' | CREATE @@ -96,18 +81,16 @@ class OperationDetailsFactorySpec extends Specification { } def 'Build policy executor patch operation details with single replace operation and #scenario.'() { - given: 'a requestParameter and a patchItem list' - def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'some uri', className: 'some class') - def pathItem = new PatchItem(op: 'REPLACE', 'path':"some uri${suffix}", value: value) + given: 'a patchItem' + def patchItem = new PatchItem(op: 'REPLACE', 'path':"some uri${suffix}", value: value) when: 'patch operation details are checked' - def result = objectUnderTest.buildOperationDetails(requestPathParameters, pathItem) + def result = objectUnderTest.buildOperationDetails(requestPathParameters, patchItem) then: 'Attribute Value in operation is correct' result.changeRequest.values()[0].attributes[0] == expectedAttributesValueInOperation where: 'attributes are set using # or resource' - scenario | suffix | value || expectedAttributesValueInOperation - 'set simple value using #' | '#/attributes/simpleAttribute' | 1 || [simpleAttribute:1] - 'set simple value using resource' | '' | simpleValueAsResource || ['simpleAttribute:1'] - 'set complex value using resource' | '' | complexValueAsResource || ["myAttribute1:myValue1","myAttribute2:myValue2"] + scenario | suffix | value || expectedAttributesValueInOperation + 'set simple value using #' | '#/attributes/attr1' | 1 || [attr1:1] + 'set complex value using resource' | '' | '{"attr1":"abc","attr2":123}' || '{"attr1":"abc","attr2":123}' } def 'Build an attribute map with different depths of hierarchy with #scenario.'() { @@ -125,30 +108,13 @@ class OperationDetailsFactorySpec extends Specification { } def 'Attempt to Build Operation details with unsupported op (MOVE).'() { - given: 'a provMnsRequestParameter and a patchItem' - def path = new RequestParameters(uriLdnFirstPart: 'some uri', className: 'some class') + given: 'a patchItem' def patchItem = new PatchItem(op: 'MOVE', 'path':'some uri') when: 'a build is attempted with an unsupported op' - objectUnderTest.buildOperationDetails(path, patchItem) + objectUnderTest.buildOperationDetails(requestPathParameters, patchItem) then: 'the result is as expected (exception thrown)' def exceptionThrown = thrown(ProvMnSException) assert exceptionThrown.title == 'Unsupported Patch Operation Type: move' } - def 'Build policy executor create operation details from ProvMnS request parameters where objectClass in resource #scenario.'() { - given: 'a provMnsRequestParameter and a resource' - def requestPathParameters = new RequestParameters(uriLdnFirstPart: 'some uri', className: 'class in uri', id:'my id') - def resource = new ResourceOneOf(id: 'some resource id', objectClass: objectInResouce) - when: 'a configurationManagementOperation is created and converted to JSON' - def result = objectUnderTest.buildCreateOperationDetails(CREATE, requestPathParameters, resource) - then: 'the result is as expected (using json to compare)' - String expectedJsonString = '{"operation":"CREATE","targetIdentifier":"some uri","changeRequest":{"' + changeRequestClassReference + '":[{"id":"my id","attributes":null}]}}' - assert jsonObjectMapper.asJsonString(result) == expectedJsonString - where: - scenario | objectInResouce || changeRequestClassReference - 'populated' | 'class in resource' || 'class in resource' - 'empty' | '' || 'class in uri' - 'null' | null || 'class in uri' - } - } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/utils/DmiDataOperationsHelperSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/utils/DmiDataOperationsHelperSpec.groovy index 943377a970..e9ebcfad33 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/utils/DmiDataOperationsHelperSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/utils/DmiDataOperationsHelperSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2023-2025 OpenInfra Foundation Europe. All rights reserved. + * Copyright (C) 2023-2026 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. @@ -55,7 +55,7 @@ class DmiDataOperationsHelperSpec extends MessagingBaseSpec { JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) @SpringBean - EventProducer eventsProducer = new EventProducer(legacyEventKafkaTemplate, cloudEventKafkaTemplate) + EventProducer eventProducer = new EventProducer(legacyEventKafkaTemplate, cloudEventKafkaTemplate) def 'Process per data operation request with #serviceName.'() { given: 'data operation request with 3 operations' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterHelperSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterHelperSpec.groovy new file mode 100644 index 0000000000..238bfdbac5 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterHelperSpec.groovy @@ -0,0 +1,105 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025-2026 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 ParameterHelperSpec extends Specification { + + def objectUnderTest = new ParameterHelper() + + 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) >> path + when: 'the request parameters are extracted' + def result = objectUnderTest.extractRequestParameters(mockHttpServletRequest) + then: 'the Uri LDN first part is as expected' + assert result.uriLdnFirstPart == expectedUriLdnFirstPart + and: 'the class name and id are mapped correctly' + assert result.className == 'myClass' + assert result.id == 'id' + where: 'The following URIs are used' + scenario | path || expectedUriLdnFirstPart + '1 segment' | 'ProvMnS/v1/segment1/myClass=id' || '/segment1' + '2 segments' | 'ProvMnS/v1/segment1/segment2/myClass=id' || '/segment1/segment2' + 'multiple segments' | 'ProvMnS/v1/segment1/segment2/segment3/segment4/myClass=id' || '/segment1/segment2/segment3/segment4' + 'no slash' | 'ProvMnS/v1/myClass=id' || '' + } + + def 'Extract request parameters for Patch Path with attributes.'() { + when: 'the request parameters are extracted from the path' + def result = objectUnderTest.createRequestParametersForPatch(path) + then: 'the FDN is as expected' + assert result.fdn == expectedFdn + and: 'the class name and id are mapped correctly' + assert result.className == 'myClass' + assert result.id == 'id' + where: 'the following paths are used' + scenario | path || expectedFdn + 'attributes in path' | '/myClass=id/attributes' || '/myClass=id' + 'attributes with parent' | '/parent=p/myClass=id/attributes' || '/parent=p/myClass=id' + '#/attributes in path' | '/myClass=id#/attributes' || '/myClass=id' + } + + def 'Attempt to extract request parameters with #scenario.'() { + given: 'a http request with invalid path' + mockHttpServletRequest.getAttribute(uriPathAttributeName) >> path + mockHttpServletRequest.getMethod() >> 'GET' + when: 'attempt to extract the request parameters' + objectUnderTest.extractRequestParameters(mockHttpServletRequest) + then: 'a ProvMnS exception is thrown' + def thrown = thrown(ProvMnSException) + assert thrown.message == 'GET failed' + and: 'the title contains the expected error message' + assert thrown.title == expectedPathInError + ' not a valid path' + where: 'the following invalid paths are used' + scenario | path || expectedPathInError + 'no = After (last) class name' | 'ProvMnS/v1/myClass1=id/Class2' || '/myClass1=id/Class2' + 'attributes in path' | 'ProvMnS/v1/myClass=id/attributes' || '/myClass=id/attributes' + '#/attributes in path' | 'ProvMnS/v1/myClass=id#/attributes' || '/myClass=id#/attributes' + 'missing ProvMnS prefix' | 'v1/segment1/myClass=id' || 'v1/segment1/myClass=id' + 'wrong version' | 'ProvMnS/wrongVersion/myClass=id' || 'ProvMnS/wrongVersion/myClass=id' + 'empty path' | '' || '' + } + + def 'Extract Fdn.'() { + expect: 'Only valid name-id pairs are retuned up to the required index' + assert objectUnderTest.extractFdn('/a=1/b=2/c=3/d/e/f', indexFromEnd) == expectedResult + where: 'following fdns are used' + indexFromEnd || expectedResult + 0 || '' + 1 || '/a=1/b=2/c=3' + 2 || '/a=1/b=2' + 3 || '/a=1' + 4 || '' + } + + def 'Extract Parent Fdn.'() { + expect: 'Teh cortect Parent FDN (up to 2nd last name-id pair)) is returned' + assert objectUnderTest.extractParentFdn('/a=1/b=2/c=3/d/e/f') == '/a=1/b=2' + } + +} 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 deleted file mode 100644 index 07ac96fe6a..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterMapperSpec.groovy +++ /dev/null @@ -1,71 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved. - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ -package org.onap.cps.ncmp.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) >> path - when: 'the request parameters are extracted' - def result = objectUnderTest.extractRequestParameters(mockHttpServletRequest) - then: 'the Uri LDN first part is as expected' - assert result.uriLdnFirstPart == expectedUriLdnFirstPart - 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 | path || expectedUriLdnFirstPart - '1 segment' | 'ProvMnS/v1/segment1/myClass=myId' || '/segment1' - '2 segments' | 'ProvMnS/v1/segment1/segment2/myClass=myId' || '/segment1/segment2' - 'multiple segments' | 'ProvMnS/v1/segment1/segment2/segment3/segment4/myClass=myId' || '/segment1/segment2/segment3/segment4' - 'no slash' | 'ProvMnS/v1/myClass=myId' || '' - } - - def 'Attempt to extract request parameters with #scenario.'() { - given: 'a http request with invalid path' - mockHttpServletRequest.getAttribute(uriPathAttributeName) >> path - mockHttpServletRequest.getMethod() >> 'GET' - when: 'attempt to extract the request parameters' - objectUnderTest.extractRequestParameters(mockHttpServletRequest) - then: 'a ProvMnS exception is thrown' - def thrown = thrown(ProvMnSException) - assert thrown.message == 'GET failed' - and: 'the title contains the expected error message' - assert thrown.title == path + ' not a valid path' - where: 'the following invalid paths are used' - scenario | path - 'no = After (last) class name'| 'ProvMnS/v1/someOtherClass=someId/myClass' - 'missing ProvMnS prefix' | 'v1/segment1/myClass=myId' - 'wrong version' | 'ProvMnS/wrongVersion/myClass=myId' - 'empty path' | '' - 'multiple ProvMnS segments' | 'ProvMnS/v1/myClass=myId/ProvMnS/v2/otherSegment' - } -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/RequestParametersSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/RequestParametersSpec.groovy deleted file mode 100644 index fe8f186a52..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/RequestParametersSpec.groovy +++ /dev/null @@ -1,42 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved. - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ -package org.onap.cps.ncmp.impl.provmns - -import spock.lang.Specification - -class RequestParametersSpec extends Specification { - - def objectUnderTest = new RequestParameters() - - def 'Generate target FDN #scenario.'() { - given: 'request parameters with URI LDN first part, class name and id' - objectUnderTest.uriLdnFirstPart = uriLdnFirstPart - objectUnderTest.className = 'myClass' - objectUnderTest.id = 'myId' - when: 'target FDN is generated' - def result = objectUnderTest.toTargetFdn() - then: 'the target FDN is as expected' - result == expectedTargetFdn - where: 'the following uri first part is used' - scenario | uriLdnFirstPart || expectedTargetFdn - 'with segments' | '/segment1' || '/segment1/myClass=myId' - 'empty first part' | '' || '/myClass=myId' - } -} diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index deafbf50ad..432633574e 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -198,9 +198,9 @@ services: image: ${DOCKER_REPO:-nexus3.onap.org:10003}/onap/policy-executor-stub:latest ports: - ${POLICY_EXECUTOR_STUB_PORT:-8785}:8093 - ### DEBUG: Uncomment next lines to enable java debugging in Policy Executor Stub - ### - ${POLICY_EXECUTOR_STUB_DEBUG_PORT:-5005}:5005 - ### environment: + ### DEBUG: Uncomment next lines to enable java debugging in Policy Executor Stub + ### - ${POLICY_EXECUTOR_STUB_DEBUG_PORT:-5005}:5005 + ### environment: #### JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 restart: unless-stopped # Note policy-executor-stub does not have a healthcheck as it does not expose /actuator/health endpoint