diff options
27 files changed, 506 insertions, 289 deletions
diff --git a/cps-dependencies/pom.xml b/cps-dependencies/pom.xml index 69ea85917f..68f36fb839 100644 --- a/cps-dependencies/pom.xml +++ b/cps-dependencies/pom.xml @@ -2,7 +2,7 @@ <!-- ============LICENSE_START======================================================= Copyright (c) 2021 Linux Foundation. - Modifications Copyright (C) 2020-2023 Nordix Foundation + Modifications Copyright (C) 2020-2024 Nordix Foundation Modifications Copyright (C) 2022 Bell Canada. ================================================================================ Licensed under the Apache License, Version 2.0 (the "License"); @@ -130,7 +130,7 @@ <dependency> <groupId>com.github.spotbugs</groupId> <artifactId>spotbugs</artifactId> - <version>4.2.0</version> + <version>4.2.3</version> </dependency> <dependency> <groupId>com.google.code.gson</groupId> diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java index 66c159105a..93cbccf1a8 100755 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java @@ -44,6 +44,7 @@ import org.onap.cps.ncmp.api.impl.inventory.CompositeState; import org.onap.cps.ncmp.api.impl.operations.DatastoreType; import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel; import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters; +import org.onap.cps.ncmp.api.models.CmResourceAddress; import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; import org.onap.cps.ncmp.rest.api.NetworkCmProxyApi; import org.onap.cps.ncmp.rest.controller.handlers.NcmpCachedResourceRequestHandler; @@ -107,8 +108,9 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { final Boolean includeDescendants, final String authorization) { final NcmpDatastoreRequestHandler ncmpDatastoreRequestHandler = getNcmpDatastoreRequestHandler(datastoreName); - return ncmpDatastoreRequestHandler.executeRequest(datastoreName, cmHandle, resourceIdentifier, - optionsParamInQuery, topicParamInQuery, includeDescendants, authorization); + final CmResourceAddress cmResourceAddress = new CmResourceAddress(datastoreName, cmHandle, resourceIdentifier); + return ncmpDatastoreRequestHandler.executeRequest(cmResourceAddress, optionsParamInQuery, topicParamInQuery, + includeDescendants, authorization); } @Override diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpCachedResourceRequestHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpCachedResourceRequestHandler.java index 430c0996f9..e6d6faf983 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpCachedResourceRequestHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpCachedResourceRequestHandler.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 Nordix Foundation + * Copyright (C) 2022-2024 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ package org.onap.cps.ncmp.rest.controller.handlers; import java.util.function.Supplier; import org.onap.cps.ncmp.api.NetworkCmProxyDataService; import org.onap.cps.ncmp.api.NetworkCmProxyQueryService; +import org.onap.cps.ncmp.api.models.CmResourceAddress; import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor; import org.onap.cps.spi.FetchDescendantsOption; import org.springframework.http.ResponseEntity; @@ -68,9 +69,7 @@ public class NcmpCachedResourceRequestHandler extends NcmpDatastoreRequestHandle } @Override - protected Supplier<Object> getTaskSupplierForGetRequest(final String datastoreName, - final String cmHandleId, - final String resourceIdentifier, + protected Supplier<Object> getTaskSupplierForGetRequest(final CmResourceAddress cmResourceAddress, final String optionsParamInQuery, final String topicParamInQuery, final String requestId, @@ -79,8 +78,7 @@ public class NcmpCachedResourceRequestHandler extends NcmpDatastoreRequestHandle final FetchDescendantsOption fetchDescendantsOption = getFetchDescendantsOption(includeDescendants); - return () -> networkCmProxyDataService.getResourceDataForCmHandle(datastoreName, cmHandleId, resourceIdentifier, - fetchDescendantsOption); + return () -> networkCmProxyDataService.getResourceDataForCmHandle(cmResourceAddress, fetchDescendantsOption); } private Supplier<Object> getTaskSupplierForQueryRequest(final String cmHandleId, diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandler.java index 65410d3a36..1ae16820a1 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandler.java @@ -25,6 +25,7 @@ import java.util.UUID; import java.util.function.Supplier; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.api.models.CmResourceAddress; import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor; import org.onap.cps.ncmp.rest.util.TopicValidator; import org.springframework.beans.factory.annotation.Value; @@ -50,18 +51,14 @@ public abstract class NcmpDatastoreRequestHandler { /** * Executes synchronous/asynchronous get request for given cm handle. * - * @param datastoreName the name of the datastore - * @param cmHandleId the cm handle - * @param resourceIdentifier the resource identifier + * @param cmResourceAddress the name of the datastore, cm handle and resource identifier * @param optionsParamInQuery the options param in query * @param topicParamInQuery the topic param in query * @param includeDescendants whether include descendants * @param authorization contents of Authorization header, or null if not present * @return the response entity */ - public ResponseEntity<Object> executeRequest(final String datastoreName, - final String cmHandleId, - final String resourceIdentifier, + public ResponseEntity<Object> executeRequest(final CmResourceAddress cmResourceAddress, final String optionsParamInQuery, final String topicParamInQuery, final boolean includeDescendants, @@ -69,16 +66,16 @@ public abstract class NcmpDatastoreRequestHandler { final boolean asyncResponseRequested = topicParamInQuery != null; if (asyncResponseRequested && notificationFeatureEnabled) { - return executeAsyncTaskAndGetResponseEntity(datastoreName, cmHandleId, resourceIdentifier, - optionsParamInQuery, topicParamInQuery, includeDescendants, authorization); + return executeAsyncTaskAndGetResponseEntity(cmResourceAddress, optionsParamInQuery, topicParamInQuery, + includeDescendants, authorization); } if (asyncResponseRequested) { log.warn("Asynchronous request is unavailable as notification feature is currently disabled, " + "will use synchronous operation."); } - final Supplier<Object> taskSupplier = getTaskSupplierForGetRequest(datastoreName, cmHandleId, - resourceIdentifier, optionsParamInQuery, NO_TOPIC, NO_REQUEST_ID, includeDescendants, authorization); + final Supplier<Object> taskSupplier = getTaskSupplierForGetRequest(cmResourceAddress, optionsParamInQuery, + NO_TOPIC, NO_REQUEST_ID, includeDescendants, authorization); return executeTaskSync(taskSupplier); } @@ -96,23 +93,18 @@ public abstract class NcmpDatastoreRequestHandler { return ResponseEntity.ok(taskSupplier.get()); } - private ResponseEntity<Object> executeAsyncTaskAndGetResponseEntity(final String datastoreName, - final String cmHandleId, - final String resourceIdentifier, + private ResponseEntity<Object> executeAsyncTaskAndGetResponseEntity(final CmResourceAddress cmResourceAddress, final String optionsParamInQuery, final String topicParamInQuery, final boolean includeDescendants, final String authorization) { final String requestId = UUID.randomUUID().toString(); - final Supplier<Object> taskSupplier = getTaskSupplierForGetRequest(datastoreName, cmHandleId, - resourceIdentifier, optionsParamInQuery, topicParamInQuery, requestId, includeDescendants, - authorization); + final Supplier<Object> taskSupplier = getTaskSupplierForGetRequest(cmResourceAddress, + optionsParamInQuery, topicParamInQuery, requestId, includeDescendants, authorization); return executeTaskAsync(topicParamInQuery, requestId, taskSupplier); } - protected abstract Supplier<Object> getTaskSupplierForGetRequest(final String datastoreName, - final String cmHandleId, - final String resourceIdentifier, + protected abstract Supplier<Object> getTaskSupplierForGetRequest(final CmResourceAddress cmResourceAddress, final String optionsParamInQuery, final String topicParamInQuery, final String requestId, diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java index 430b749eff..75112caf14 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 Nordix Foundation + * Copyright (C) 2022-2024 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.onap.cps.ncmp.api.NetworkCmProxyDataService; import org.onap.cps.ncmp.api.impl.exception.InvalidDatastoreException; import org.onap.cps.ncmp.api.impl.operations.DatastoreType; import org.onap.cps.ncmp.api.impl.operations.OperationType; +import org.onap.cps.ncmp.api.models.CmResourceAddress; import org.onap.cps.ncmp.api.models.DataOperationRequest; import org.onap.cps.ncmp.rest.exceptions.OperationNotSupportedException; import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor; @@ -77,18 +78,15 @@ public class NcmpPassthroughResourceRequestHandler extends NcmpDatastoreRequestH } @Override - protected Supplier<Object> getTaskSupplierForGetRequest(final String datastoreName, - final String cmHandleId, - final String resourceIdentifier, + protected Supplier<Object> getTaskSupplierForGetRequest(final CmResourceAddress cmResourceAddress, final String optionsParamInQuery, final String topicParamInQuery, final String requestId, final boolean includeDescendants, final String authorization) { - return () -> networkCmProxyDataService.getResourceDataForCmHandle( - datastoreName, cmHandleId, resourceIdentifier, optionsParamInQuery, topicParamInQuery, requestId, - authorization); + return () -> networkCmProxyDataService.getResourceDataForCmHandle(cmResourceAddress, optionsParamInQuery, + topicParamInQuery, requestId, authorization); } private ResponseEntity<Object> getRequestIdAndSendDataOperationRequestToDmiService( diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy index 616492d4e2..a5b1f05ee1 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy @@ -39,6 +39,7 @@ import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle +import org.onap.cps.ncmp.api.models.CmResourceAddress import org.onap.cps.ncmp.rest.controller.handlers.NcmpCachedResourceRequestHandler import org.onap.cps.ncmp.rest.controller.handlers.NcmpPassthroughResourceRequestHandler import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor @@ -136,15 +137,15 @@ class NetworkCmProxyControllerSpec extends Specification { def NO_TOPIC = null def NO_REQUEST_ID = null def NO_AUTH_HEADER = null - def TIMOUT_FOR_TEST = 1234 + def TIMEOUT_FOR_TEST = 1234 def logger = Spy(ListAppender<ILoggingEvent>) def setup() { ncmpCachedResourceRequestHandler.notificationFeatureEnabled = true - ncmpCachedResourceRequestHandler.timeOutInMilliSeconds = TIMOUT_FOR_TEST + ncmpCachedResourceRequestHandler.timeOutInMilliSeconds = TIMEOUT_FOR_TEST ncmpPassthroughResourceRequestHandler.notificationFeatureEnabled = true - ncmpPassthroughResourceRequestHandler.timeOutInMilliSeconds = TIMOUT_FOR_TEST + ncmpPassthroughResourceRequestHandler.timeOutInMilliSeconds = TIMEOUT_FOR_TEST setupLogger() } @@ -154,31 +155,28 @@ class NetworkCmProxyControllerSpec extends Specification { def 'Get Resource Data from pass-through operational.'() { given: 'resource data url' - def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" + - "?resourceIdentifier=parent/child&options=(a=1,b=2)" + def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational?resourceIdentifier=parent/child&options=(a=1,b=2)" + and: 'the expected cm resource address' + def expectedCmResourceAddress = new CmResourceAddress(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle', 'parent/child') when: 'get data resource request is performed' - def response = mvc.perform( - get(getUrl) - .contentType(MediaType.APPLICATION_JSON) - ).andReturn().response - then: 'the NCMP data service is called with getResourceDataOperationalForCmHandle' - 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle', - 'parent/child','(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) + def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response + then: 'the NCMP data service is called with correct parameters' + 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(expectedCmResourceAddress, '(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) and: 'response status is Ok' - response.status == HttpStatus.OK.value() + assert response.status == HttpStatus.OK.value() } def 'Get Resource Data from ncmp-datastore:operational (cached) parameters handling with #scenario.'() { given: 'resource data url' - def getUrl = "$ncmpBasePathV1/ch/h123/data/ds/ncmp-datastore:operational" + - "?resourceIdentifier=parent/child${additionalUrlParam}" + def getUrl = "$ncmpBasePathV1/ch/h123/data/ds/ncmp-datastore:operational?resourceIdentifier=parent/child${additionalUrlParam}" + and: 'the expected cm resource address' + def expectedCmResourceAddress = new CmResourceAddress('ncmp-datastore:operational', 'h123', 'parent/child') when: 'get data resource request is performed' - def response = mvc.perform( - get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response - then: 'task executor is called appropriate number of times' - 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle('ncmp-datastore:operational', 'h123', 'parent/child', expectedIncludeDescendants) + def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response + then: 'the NCMP data service is called with correct parameters' + 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(expectedCmResourceAddress, expectedIncludeDescendants) and: 'response status is OK' - response.status == HttpStatus.OK.value() + assert response.status == HttpStatus.OK.value() where: 'the following parameters are used' scenario | additionalUrlParam || expectedIncludeDescendants 'no additional param' | '' || OMIT_DESCENDANTS @@ -192,15 +190,11 @@ class NetworkCmProxyControllerSpec extends Specification { def 'Execute (async) data operation to read data from dmi service.'() { given: 'data operation url' def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name" - def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(getDataOperationRequest("read", datastore.datastoreName)) + def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(getDataOperationRequest('read', datastore.datastoreName)) when: 'post data operation request is performed' - def response = mvc.perform( - post(getUrl) - .contentType(MediaType.APPLICATION_JSON) - .content(dataOperationRequestJsonData) - ).andReturn().response + def response = mvc.perform(post(getUrl).contentType(MediaType.APPLICATION_JSON).content(dataOperationRequestJsonData)).andReturn().response then: 'response status is Ok' - response.status == HttpStatus.OK.value() + assert response.status == HttpStatus.OK.value() and: 'async request id is generated' assert response.contentAsString.contains('requestId') then: 'the request is handled asynchronously' @@ -212,80 +206,57 @@ class NetworkCmProxyControllerSpec extends Specification { def 'Execute (async) data operation with some validation error.'() { given: 'data operation url' def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name" - def dataOperationRequestJsonData = jsonObjectMapper.asJsonString( - getDataOperationRequest('read', 'invalid datastore')) + def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(getDataOperationRequest('read', 'invalid datastore')) when: 'post data resource request is performed' - def response = mvc.perform( - post(getUrl) - .contentType(MediaType.APPLICATION_JSON) - .content(dataOperationRequestJsonData) - ).andReturn().response + def response = mvc.perform(post(getUrl).contentType(MediaType.APPLICATION_JSON).content(dataOperationRequestJsonData)).andReturn().response then: 'response status is BAD_REQUEST' - response.status == HttpStatus.BAD_REQUEST.value() + assert response.status == HttpStatus.BAD_REQUEST.value() } def 'Get data operation resource data when notification feature is disabled for datastore: #datastore.'() { given: 'data operation url' def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name" - def dataOperationRequestJsonData = jsonObjectMapper.asJsonString( - getDataOperationRequest("read", PASSTHROUGH_RUNNING.datastoreName)) + def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(getDataOperationRequest("read", PASSTHROUGH_RUNNING.datastoreName)) ncmpPassthroughResourceRequestHandler.notificationFeatureEnabled = false when: 'post data resource request is performed' - def response = mvc.perform( - post(getUrl) - .contentType(MediaType.APPLICATION_JSON) - .content(dataOperationRequestJsonData) + def response = mvc.perform(post(getUrl).contentType(MediaType.APPLICATION_JSON).content(dataOperationRequestJsonData) ).andReturn().response then: 'response status is Ok' - response.status == HttpStatus.OK.value() + assert response.status == HttpStatus.OK.value() and: 'async request id is unavailable' assert response.contentAsString == '{"status":"Asynchronous request is unavailable as notification feature is currently disabled."}' } def 'Query Resource Data from operational.'() { given: 'the query resource data url' - def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational/query" + - "?cps-path=/cps/path" + def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational/query?cps-path=/cps/path" when: 'the query data resource request is performed' - def response = mvc.perform( - get(getUrl) - .contentType(MediaType.APPLICATION_JSON) - ).andReturn().response + def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response then: 'the NCMP query service is called with queryResourceDataOperationalForCmHandle' - 1 * mockNetworkCmProxyQueryService.queryResourceDataOperational('testCmHandle', - '/cps/path', - FetchDescendantsOption.OMIT_DESCENDANTS) + 1 * mockNetworkCmProxyQueryService.queryResourceDataOperational('testCmHandle','/cps/path',FetchDescendantsOption.OMIT_DESCENDANTS) and: 'response status is Ok' - response.status == HttpStatus.OK.value() + assert response.status == HttpStatus.OK.value() } def 'Query Resource Data with unsupported datastore'() { given: 'the query resource data url' - def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running/query" + - "?cps-path=/cps/path" + def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running/query?cps-path=/cps/path" when: 'the query data resource request is performed' - def response = mvc.perform( - get(getUrl) - .contentType(MediaType.APPLICATION_JSON) - ).andReturn().response + def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response then: 'a 400 BAD_REQUEST is returned for the unsupported datastore' - response.status == 400 + assert response.status == 400 and: 'the error message is that the datastore is not supported' - response.contentAsString.contains("ncmp-datastore:passthrough-running is not supported") + assert response.contentAsString.contains("ncmp-datastore:passthrough-running is not supported") } def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() { given: 'resource data url' - def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" + - "?resourceIdentifier=" + resourceIdentifier + "&options=(a=1,b=2)" + def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=$resourceIdentifier&options=(a=1,b=2)" and: 'ncmp service returns json object' - mockNetworkCmProxyDataService.getResourceDataForCmHandle(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', - resourceIdentifier,'(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) >> '{valid-json}' + def expectedCmResourceAddress = new CmResourceAddress(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', resourceIdentifier) + mockNetworkCmProxyDataService.getResourceDataForCmHandle(expectedCmResourceAddress,'(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) >> '{valid-json}' when: 'get data resource request is performed' - def response = mvc.perform( - get(getUrl) - .contentType(MediaType.APPLICATION_JSON) - ).andReturn().response + def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response then: 'response status is Ok' response.status == HttpStatus.OK.value() and: 'response contains valid object body' @@ -302,34 +273,24 @@ class NetworkCmProxyControllerSpec extends Specification { def 'Update resource data from pass-through running.'() { given: 'update resource data url' - def updateUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" + - "?resourceIdentifier=parent/child" + def updateUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=parent/child" when: 'update data resource request is performed' - def response = mvc.perform( - put(updateUrl) - .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody) - ).andReturn().response + def response = mvc.perform(put(updateUrl).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)).andReturn().response then: 'ncmp service method to update resource is called' - 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', - 'parent/child', UPDATE, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER) + 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle','parent/child', UPDATE, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER) and: 'the response status is OK' - response.status == HttpStatus.OK.value() + assert response.status == HttpStatus.OK.value() } def 'Create Resource Data from pass-through running with #scenario.'() { given: 'resource data url' - def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" + - "?resourceIdentifier=parent/child" + def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=parent/child" when: 'create resource request is performed' - def response = mvc.perform( - post(url) - .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody) - ).andReturn().response + def response = mvc.perform(post(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)).andReturn().response then: 'ncmp service method to create resource called' - 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', - 'parent/child', CREATE, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER) + 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', 'parent/child', CREATE, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER) and: 'resource is created' - response.status == HttpStatus.CREATED.value() + assert response.status == HttpStatus.CREATED.value() } def 'Get module references for the given dataspace and cm handle.'() { @@ -338,12 +299,11 @@ class NetworkCmProxyControllerSpec extends Specification { when: 'get module resource request is performed' def response = mvc.perform(get(getUrl)).andReturn().response then: 'ncmp service method to get yang resource module references is called' - mockNetworkCmProxyDataService.getYangResourcesModuleReferences('some-cmhandle') - >> [new ModuleReference(moduleName: 'some-name1', revision: '2021-10-03')] + mockNetworkCmProxyDataService.getYangResourcesModuleReferences('some-cmhandle') >> [new ModuleReference(moduleName: 'some-name1', revision: '2021-10-03')] and: 'response contains an array with the module name and revision' response.getContentAsString() == '[{"moduleName":"some-name1","revision":"2021-10-03"}]' and: 'response returns an OK http code' - response.status == HttpStatus.OK.value() + assert response.status == HttpStatus.OK.value() } def 'Retrieve cm handles.'() { @@ -364,13 +324,11 @@ class NetworkCmProxyControllerSpec extends Specification { and: 'map for trust level per cmHandle has value for only one cm handle' trustLevelPerCmHandle.put('ch-1', TrustLevel.NONE) when: 'the searches api is invoked' - def response = mvc.perform(post(searchesEndpoint) - .contentType(MediaType.APPLICATION_JSON) - .content(jsonString)).andReturn().response + def response = mvc.perform(post(searchesEndpoint).contentType(MediaType.APPLICATION_JSON).content(jsonString)).andReturn().response then: 'response status returns OK' - response.status == HttpStatus.OK.value() + assert response.status == HttpStatus.OK.value() and: 'the expected response content is returned' - response.contentAsString == '[{"cmHandle":"ch-1","publicCmHandleProperties":[{"color":"yellow"}],"state":null,"trustLevel":"NONE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null},{"cmHandle":"ch-2","publicCmHandleProperties":[{"color":"green"}],"state":null,"trustLevel":null,"moduleSetTag":"someModuleSetTag","alternateId":"someAlternateId","dataProducerIdentifier":"someDataProducerIdentifier"}]' + assert response.contentAsString == '[{"cmHandle":"ch-1","publicCmHandleProperties":[{"color":"yellow"}],"state":null,"trustLevel":"NONE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null},{"cmHandle":"ch-2","publicCmHandleProperties":[{"color":"green"}],"state":null,"trustLevel":null,"moduleSetTag":"someModuleSetTag","alternateId":"someAlternateId","dataProducerIdentifier":"someDataProducerIdentifier"}]' } def 'Get complete Cm Handle details by Cm Handle id.'() { @@ -396,7 +354,7 @@ class NetworkCmProxyControllerSpec extends Specification { and: 'the response contains the cm handle state' assertContainsState(response) and: 'the content does not contain dmi properties' - !response.contentAsString.contains("some DMI property") + assert !response.contentAsString.contains("some DMI property") } def 'Get Cm Handle public properties by Cm Handle id.'() { @@ -405,13 +363,11 @@ class NetworkCmProxyControllerSpec extends Specification { and: 'some cm handle public properties' def publicProperties = ['public prop': 'some public property'] and: 'the service method is invoked with the cm handle id returning the cm handle public properties' - 1 * mockNetworkCmProxyDataService - .getCmHandlePublicProperties('some-cm-handle') >> publicProperties + 1 * mockNetworkCmProxyDataService.getCmHandlePublicProperties('some-cm-handle') >> publicProperties when: 'the cm handle properties api is invoked' - def response = mvc.perform( - get(cmHandlePropertiesEndpoint)).andReturn().response + def response = mvc.perform(get(cmHandlePropertiesEndpoint)).andReturn().response then: 'the correct response is returned' - response.status == HttpStatus.OK.value() + assert response.status == HttpStatus.OK.value() and: 'the response contains the public properties' assertContainsPublicProperties(response) } @@ -422,15 +378,13 @@ class NetworkCmProxyControllerSpec extends Specification { and: 'some cm handle composite state' def compositeState = compositeStateTestObject() and: 'the service method is invoked with the cm handle id returning the cm handle composite state' - 1 * mockNetworkCmProxyDataService - .getCmHandleCompositeState('some-cm-handle') >> compositeState + 1 * mockNetworkCmProxyDataService.getCmHandleCompositeState('some-cm-handle') >> compositeState when: 'the cm handle state api is invoked' - def response = mvc.perform( - get(cmHandlePropertiesEndpoint)).andReturn().response + def response = mvc.perform(get(cmHandlePropertiesEndpoint)).andReturn().response then: 'the correct response is returned' response.status == HttpStatus.OK.value() and: 'the response contains the cm handle state' - assertContainsState(response) + assert assertContainsState(response) } def 'Call execute cm handle searches with unrecognized condition name.'() { @@ -449,12 +403,9 @@ class NetworkCmProxyControllerSpec extends Specification { trustLevelPerCmHandle.put('ch-1', TrustLevel.COMPLETE) trustLevelPerCmHandle.put('ch-2', TrustLevel.NONE) when: 'the searches api is invoked' - def response = mvc.perform( - post(searchesEndpoint) - .contentType(MediaType.APPLICATION_JSON) - .content(jsonString)).andReturn().response + def response = mvc.perform(post(searchesEndpoint).contentType(MediaType.APPLICATION_JSON).content(jsonString)).andReturn().response then: 'an empty cm handle identifier is returned' - response.contentAsString == '[{"cmHandle":"ch-1","publicCmHandleProperties":[{"color":"yellow"}],"state":null,"trustLevel":"COMPLETE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null},{"cmHandle":"ch-2","publicCmHandleProperties":[{"color":"green"}],"state":null,"trustLevel":"NONE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null}]' + assert response.contentAsString == '[{"cmHandle":"ch-1","publicCmHandleProperties":[{"color":"yellow"}],"state":null,"trustLevel":"COMPLETE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null},{"cmHandle":"ch-2","publicCmHandleProperties":[{"color":"green"}],"state":null,"trustLevel":"NONE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null}]' } def 'Query for cm handles matching query parameters'() { @@ -463,68 +414,47 @@ class NetworkCmProxyControllerSpec extends Specification { and: 'the service method is invoked with module names and returns cm handle ids' 1 * mockNetworkCmProxyDataService.executeCmHandleIdSearch(_) >> ['ch-1', 'ch-2'] when: 'the searches api is invoked' - def response = mvc.perform( - post(searchesEndpoint) - .contentType(MediaType.APPLICATION_JSON) - .content('{}')).andReturn().response + def response = mvc.perform(post(searchesEndpoint).contentType(MediaType.APPLICATION_JSON).content('{}')).andReturn().response then: 'cm handle ids are returned' - response.contentAsString == '["ch-1","ch-2"]' + assert response.contentAsString == '["ch-1","ch-2"]' } def 'Query for cm handles with invalid request payload'() { when: 'the searches api is invoked' def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches" def invalidInputData = '{invalidJson}' - def response = mvc.perform( - post(searchesEndpoint) - .contentType(MediaType.APPLICATION_JSON) - .content(invalidInputData)).andReturn().response + def response = mvc.perform(post(searchesEndpoint).contentType(MediaType.APPLICATION_JSON).content(invalidInputData)).andReturn().response then: 'BAD_REQUEST is returned' - response.getStatus() == 400 + assert response.getStatus() == 400 } def 'Patch resource data in pass-through running datastore.'() { given: 'patch resource data url' - def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" + - "?resourceIdentifier=parent/child" + def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=parent/child" when: 'patch data resource request is performed' - def response = mvc.perform( - patch(url) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON).content(requestBody) - ).andReturn().response + def response = mvc.perform(patch(url).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).content(requestBody)).andReturn().response then: 'ncmp service method to update resource is called' - 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', - 'parent/child', PATCH, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER) + 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', 'parent/child', PATCH, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER) and: 'the response status is OK' - response.status == HttpStatus.OK.value() + assert response.status == HttpStatus.OK.value() } def 'Delete resource data in pass-through running datastore.'() { given: 'delete resource data url' - def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" + - "?resourceIdentifier=parent/child" + def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=parent/child" when: 'delete data resource request is performed' - def response = mvc.perform( - delete(url) - .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andReturn().response + def response = mvc.perform(delete(url).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andReturn().response then: 'the ncmp service method to delete resource is called (with null as body)' - 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', - 'parent/child', DELETE, null, 'application/json;charset=UTF-8', NO_AUTH_HEADER) + 1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', 'parent/child', DELETE, null, 'application/json;charset=UTF-8', NO_AUTH_HEADER) and: 'the response is No Content' - response.status == HttpStatus.NO_CONTENT.value() + assert response.status == HttpStatus.NO_CONTENT.value() } def 'Get resource data from DMI with valid topic i.e. async request for #scenario'() { given: 'resource data url' - def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" + - "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=my-topic-name" + def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=my-topic-name" when: 'get data resource request is performed' - def response = mvc.perform( - get(getUrl) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON_VALUE) - ).andReturn().response + def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON_VALUE)).andReturn().response then: 'async request id is generated' assert response.contentAsString.contains("requestId") where: 'the following parameters are used' @@ -535,17 +465,14 @@ class NetworkCmProxyControllerSpec extends Specification { def 'Getting module definitions for a module'() { when: 'get module definition request is performed with module name' - def response = mvc.perform( - get("$ncmpBasePathV1/ch/some-cmhandle/modules/definitions?module-name=sampleModuleName")) - .andReturn().response + def response = mvc.perform(get("$ncmpBasePathV1/ch/some-cmhandle/modules/definitions?module-name=sampleModuleName")).andReturn().response then: 'ncmp service method is invoked with correct parameters' mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleAndModule('some-cmhandle', 'sampleModuleName', _) - >> [new ModuleDefinition('sampleModuleName', '2021-10-03', - 'module sampleModuleName{ sample module content }')] + >> [new ModuleDefinition('sampleModuleName', '2021-10-03','module sampleModuleName{ sample module content }')] and: 'response contains an array with the module name, revision and content' response.getContentAsString() == '[{"moduleName":"sampleModuleName","revision":"2021-10-03","content":"module sampleModuleName{ sample module content }"}]' and: 'response returns an OK http code' - response.status == HttpStatus.OK.value() + assert response.status == HttpStatus.OK.value() } def 'Getting module definitions filtering on #scenario'() { @@ -590,17 +517,15 @@ class NetworkCmProxyControllerSpec extends Specification { def 'Get Resource Data from operational with or without descendants'() { given: 'resource data url with descendants #enabled' - def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational" + - "?resourceIdentifier=parent/child&include-descendants=${booleanValue}" + def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational?resourceIdentifier=parent/child&include-descendants=${booleanValue}" + and: 'the expected target' + def expectedCmResourceAddress = new CmResourceAddress(OPERATIONAL.datastoreName, 'testCmHandle', 'parent/child') when: 'get data resource request is performed' - def response = mvc.perform( - get(getUrl) - .contentType(MediaType.APPLICATION_JSON) - ).andReturn().response + def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response then: 'the NCMP data service is called with getResourceDataOperational with #descendantsOption' - 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(OPERATIONAL.datastoreName, 'testCmHandle', 'parent/child', descendantsOption) + 1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(expectedCmResourceAddress, descendantsOption) and: 'response status is Ok' - response.status == HttpStatus.OK.value() + assert response.status == HttpStatus.OK.value() where: 'the following parameters are used' booleanValue | descendantsOption false | OMIT_DESCENDANTS diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandlerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandlerSpec.groovy index ddeac519c3..bdd0e716d8 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandlerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandlerSpec.groovy @@ -25,6 +25,7 @@ import org.onap.cps.ncmp.api.impl.exception.InvalidDatastoreException import org.onap.cps.ncmp.api.impl.exception.InvalidOperationException import org.onap.cps.ncmp.api.models.DataOperationDefinition import org.onap.cps.ncmp.api.models.DataOperationRequest +import org.onap.cps.ncmp.api.models.CmResourceAddress import org.onap.cps.ncmp.rest.exceptions.OperationNotSupportedException import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor import spock.lang.Specification @@ -48,12 +49,13 @@ class NcmpDatastoreRequestHandlerSpec extends Specification { objectUnderTest.notificationFeatureEnabled = notificationFeatureEnabled and: 'a flag to track the network service call' def networkServiceMethodCalled = false + and: 'a CM resource address' + def cmResourceAddress = new CmResourceAddress('ds', 'ch1', 'resource1') and: 'the (mocked) service will use the flag to indicate if it is called' - mockNetworkCmProxyDataService.getResourceDataForCmHandle('ds', 'ch1', 'resource1', 'options', _, _, NO_AUTH_HEADER) >> { - networkServiceMethodCalled = true - } + mockNetworkCmProxyDataService.getResourceDataForCmHandle(cmResourceAddress, 'options', _, _, NO_AUTH_HEADER) >> + { networkServiceMethodCalled = true } when: 'get request is executed with topic = #topic' - objectUnderTest.executeRequest('ds', 'ch1', 'resource1', 'options', topic, false, NO_AUTH_HEADER) + objectUnderTest.executeRequest(cmResourceAddress, 'options', topic, false, NO_AUTH_HEADER) then: 'the task is executed in an async fashion or not' expectedCalls * spiedCpsNcmpTaskExecutor.executeTask(*_) and: 'the service request is invoked' diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java index 4230140d26..20545d711d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java @@ -29,6 +29,7 @@ import org.onap.cps.ncmp.api.impl.inventory.CompositeState; import org.onap.cps.ncmp.api.impl.operations.OperationType; import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters; import org.onap.cps.ncmp.api.models.CmHandleQueryServiceParameters; +import org.onap.cps.ncmp.api.models.CmResourceAddress; import org.onap.cps.ncmp.api.models.DataOperationRequest; import org.onap.cps.ncmp.api.models.DmiPluginRegistration; import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse; @@ -53,18 +54,14 @@ public interface NetworkCmProxyDataService { /** * Get resource data for given data store using dmi. * - * @param datastoreName datastore name - * @param cmHandleId cm handle identifier - * @param resourceIdentifier resource identifier + * @param cmResourceAddress target datastore, cm handle and resource identifier * @param optionsParamInQuery options query * @param topicParamInQuery topic name for (triggering) async responses * @param requestId unique requestId for async request * @param authorization contents of Authorization header, or null if not present * @return {@code Object} resource data */ - Object getResourceDataForCmHandle(String datastoreName, - String cmHandleId, - String resourceIdentifier, + Object getResourceDataForCmHandle(CmResourceAddress cmResourceAddress, String optionsParamInQuery, String topicParamInQuery, String requestId, @@ -73,15 +70,11 @@ public interface NetworkCmProxyDataService { /** * Get resource data for operational. * - * @param datastoreName datastore name - * @param cmHandleId cm handle identifier - * @param resourceIdentifier resource identifier + * @param cmResourceAddress target datastore, cm handle and resource identifier * @Link FetchDescendantsOption fetch descendants option * @return {@code Object} resource data */ - Object getResourceDataForCmHandle(String datastoreName, - String cmHandleId, - String resourceIdentifier, + Object getResourceDataForCmHandle(CmResourceAddress cmResourceAddress, FetchDescendantsOption fetchDescendantsOption); /** @@ -110,11 +103,11 @@ public interface NetworkCmProxyDataService { * @return {@code Object} return data */ Object writeResourceDataPassThroughRunningForCmHandle(String cmHandleId, - String resourceIdentifier, - OperationType operationType, - String requestBody, - String contentType, - String authorization); + String resourceIdentifier, + OperationType operationType, + String requestBody, + String contentType, + String authorization); /** * Retrieve module references for the given cm handle. diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java index 6ab6eab653..c15df9c869 100755 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java @@ -72,6 +72,7 @@ import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters; import org.onap.cps.ncmp.api.models.CmHandleQueryServiceParameters; import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse; +import org.onap.cps.ncmp.api.models.CmResourceAddress; import org.onap.cps.ncmp.api.models.DataOperationRequest; import org.onap.cps.ncmp.api.models.DmiPluginRegistration; import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse; @@ -127,15 +128,12 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService } @Override - public Object getResourceDataForCmHandle(final String datastoreName, - final String cmHandleId, - final String resourceIdentifier, + public Object getResourceDataForCmHandle(final CmResourceAddress cmResourceAddress, final String optionsParamInQuery, final String topicParamInQuery, final String requestId, final String authorization) { - final ResponseEntity<?> responseEntity = dmiDataOperations.getResourceDataFromDmi(datastoreName, cmHandleId, - resourceIdentifier, + final ResponseEntity<?> responseEntity = dmiDataOperations.getResourceDataFromDmi(cmResourceAddress, optionsParamInQuery, topicParamInQuery, requestId, @@ -144,12 +142,12 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService } @Override - public Object getResourceDataForCmHandle(final String datastoreName, - final String cmHandleId, - final String resourceIdentifier, + public Object getResourceDataForCmHandle(final CmResourceAddress cmResourceAddress, final FetchDescendantsOption fetchDescendantsOption) { - return cpsDataService.getDataNodes(datastoreName, cmHandleId, resourceIdentifier, - fetchDescendantsOption).iterator().next(); + return cpsDataService.getDataNodes(cmResourceAddress.datastoreName(), + cmResourceAddress.cmHandleId(), + cmResourceAddress.resourceIdentifier(), + fetchDescendantsOption).iterator().next(); } @Override diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceService.java index 38f3db98de..6b02adb654 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceService.java @@ -57,4 +57,15 @@ public interface CmNotificationSubscriptionPersistenceService { */ Collection<String> getOngoingCmNotificationSubscriptionIds(final DatastoreType datastoreType, final String cmHandleId, final String xpath); + + /** + * Add or update cm notification subscription. + * + * @param datastoreType valid datastore type + * @param cmHandle cmhandle id + * @param xpath valid xpath + * @param newSubscriptionId subscription Id to be added + */ + void addOrUpdateCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandle, + final String xpath, final String newSubscriptionId); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImpl.java index 6e4997a4dd..5eca5e8c57 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImpl.java @@ -20,15 +20,26 @@ package org.onap.cps.ncmp.api.impl.events.cmsubscription.service; +import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING; +import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS; + +import java.io.Serializable; +import java.time.OffsetDateTime; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.onap.cps.api.CpsDataService; import org.onap.cps.api.CpsQueryService; +import org.onap.cps.cpspath.parser.CpsPathUtil; import org.onap.cps.ncmp.api.impl.operations.DatastoreType; -import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.model.DataNode; +import org.onap.cps.utils.ContentType; +import org.onap.cps.utils.JsonObjectMapper; import org.springframework.stereotype.Service; @Slf4j @@ -37,14 +48,16 @@ import org.springframework.stereotype.Service; public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotificationSubscriptionPersistenceService { private static final String SUBSCRIPTION_ANCHOR_NAME = "cm-data-subscriptions"; - private static final String IS_ONGOING_CM_SUBSCRIPTION_CPS_PATH_QUERY = """ + private static final String CM_SUBSCRIPTION_CPS_PATH_QUERY = """ /datastores/datastore[@name='%s']/cm-handles/cm-handle[@id='%s']/filters/filter[@xpath='%s'] """.trim(); private static final String SUBSCRIPTION_IDS_CPS_PATH_QUERY = """ //filter/subscriptionIds[text()='%s'] """.trim(); + private final JsonObjectMapper jsonObjectMapper; private final CpsQueryService cpsQueryService; + private final CpsDataService cpsDataService; @Override public boolean isOngoingCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId, @@ -56,7 +69,7 @@ public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotif public boolean isUniqueSubscriptionId(final String subscriptionId) { return cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, SUBSCRIPTION_IDS_CPS_PATH_QUERY.formatted(subscriptionId), - FetchDescendantsOption.OMIT_DESCENDANTS).isEmpty(); + OMIT_DESCENDANTS).isEmpty(); } @Override @@ -64,17 +77,65 @@ public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotif final String cmHandleId, final String xpath) { final String isOngoingCmSubscriptionCpsPathQuery = - IS_ONGOING_CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId, + CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId, escapeQuotesByDoublingThem(xpath)); final Collection<DataNode> existingNodes = cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME, - isOngoingCmSubscriptionCpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS); + isOngoingCmSubscriptionCpsPathQuery, OMIT_DESCENDANTS); if (existingNodes.isEmpty()) { return Collections.emptyList(); } return (List<String>) existingNodes.iterator().next().getLeaves().get("subscriptionIds"); } + @Override + public void addOrUpdateCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId, + final String xpath, final String newSubscriptionId) { + if (isOngoingCmNotificationSubscription(datastoreType, cmHandleId, xpath)) { + final DataNode existingFilterNode = + cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, + CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId, + escapeQuotesByDoublingThem(xpath)), + OMIT_DESCENDANTS).iterator().next(); + final Collection<String> existingSubscriptionIds = getOngoingCmNotificationSubscriptionIds(datastoreType, + cmHandleId, xpath); + if (!existingSubscriptionIds.contains(newSubscriptionId)) { + updateListOfSubscribers(existingSubscriptionIds, newSubscriptionId, existingFilterNode); + } + } else { + addNewSubscriptionViaDatastore(datastoreType, cmHandleId, xpath, newSubscriptionId); + } + } + + private void addNewSubscriptionViaDatastore(final DatastoreType datastoreType, final String cmHandleId, + final String xpath, final String newSubscriptionId) { + final String parentXpathFormat = "/datastores/datastore[@name='%s']/cm-handles"; + String parentXpath = ""; + if (datastoreType == PASSTHROUGH_RUNNING) { + parentXpath = parentXpathFormat.formatted("ncmp-datastore:passthrough-running"); + } else { + parentXpath = parentXpathFormat.formatted("ncmp-datastore:passthrough-operational"); + } + + final String updatedJson = String.format("{\"cm-handle\":[{\"id\":\"%s\",\"filters\":{\"filter\":" + + "[{\"xpath\":\"%s\",\"subscriptionIds\":[\"%s\"]}]}}]}", cmHandleId, xpath, newSubscriptionId); + cpsDataService.saveData(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, parentXpath, updatedJson, + OffsetDateTime.now(), ContentType.JSON); + } + + private void updateListOfSubscribers(final Collection<String> existingSubscriptionIds, + final String newSubscriptionId, final DataNode existingFilterNode) { + final String parentXpath = CpsPathUtil.getNormalizedParentXpath(existingFilterNode.getXpath()); + final List<String> updatedSubscribers = new ArrayList<>(existingSubscriptionIds); + updatedSubscribers.add(newSubscriptionId); + final Map<String, Serializable> updatedLeaves = new HashMap<>(); + updatedLeaves.put("xpath", existingFilterNode.getLeaves().get("xpath")); + updatedLeaves.put("subscriptionIds", (Serializable) updatedSubscribers); + final String updatedJson = "{\"filter\":[" + jsonObjectMapper.asJsonString(updatedLeaves) + "]}"; + cpsDataService.updateNodeLeaves(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, parentXpath, updatedJson, + OffsetDateTime.now()); + } + private static String escapeQuotesByDoublingThem(final String inputXpath) { return inputXpath.replace("'", "''"); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java index 2a4bceca23..a9ec1241bc 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java @@ -42,6 +42,7 @@ import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence; import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder; import org.onap.cps.ncmp.api.impl.utils.data.operation.ResourceDataOperationRequestUtils; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; +import org.onap.cps.ncmp.api.models.CmResourceAddress; import org.onap.cps.ncmp.api.models.DataOperationRequest; import org.onap.cps.spi.exceptions.CpsException; import org.onap.cps.utils.JsonObjectMapper; @@ -70,9 +71,7 @@ public class DmiDataOperations extends DmiOperations { * This method fetches the resource data from operational data store for given cm handle * identifier on given resource using dmi client. * - * @param dataStoreName name of data store - * @param cmHandleId network resource identifier - * @param resourceId resource identifier + * @param cmResourceAddress target datastore, cm handle and resource identifier * @param optionsParamInQuery options query * @param topicParamInQuery topic name for (triggering) async responses * @param requestId requestId for async responses @@ -82,19 +81,17 @@ public class DmiDataOperations extends DmiOperations { @Timed(value = "cps.ncmp.dmi.get", description = "Time taken to fetch the resource data from operational data store for given cm handle " + "identifier on given resource using dmi client") - public ResponseEntity<Object> getResourceDataFromDmi(final String dataStoreName, - final String cmHandleId, - final String resourceId, + public ResponseEntity<Object> getResourceDataFromDmi(final CmResourceAddress cmResourceAddress, final String optionsParamInQuery, final String topicParamInQuery, final String requestId, final String authorization) { - final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId); + final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmResourceAddress.cmHandleId()); final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState(); validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState); - final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, - yangModelCmHandle); - final String dmiResourceDataUrl = getDmiRequestUrl(dataStoreName, cmHandleId, resourceId, optionsParamInQuery, + final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle); + final String dmiResourceDataUrl = getDmiRequestUrl(cmResourceAddress.datastoreName(), + cmResourceAddress.cmHandleId(), cmResourceAddress.resourceIdentifier(), optionsParamInQuery, topicParamInQuery, yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA)); return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonRequestBody, READ, authorization); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/AlternateIdChecker.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/AlternateIdChecker.java index f14439f690..60f39fcea0 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/AlternateIdChecker.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/AlternateIdChecker.java @@ -115,23 +115,7 @@ public class AlternateIdChecker { for (final NcmpServiceCmHandle ncmpServiceCmHandle : newNcmpServiceCmHandles) { final String cmHandleId = ncmpServiceCmHandle.getCmHandleId(); final String proposedAlternateId = ncmpServiceCmHandle.getAlternateId(); - final boolean isAcceptable; - if (StringUtils.isEmpty(proposedAlternateId)) { - isAcceptable = true; - } else { - if (acceptedAlternateIds.contains(proposedAlternateId)) { - isAcceptable = false; - log.warn("Alternate id update ignored, cannot update cm handle {}, alternate id is already " - + "assigned to a different cm handle (in this batch)", cmHandleId); - } else { - if (Operation.CREATE.equals(operation)) { - isAcceptable = canApplyAlternateId(cmHandleId, NO_CURRENT_ALTERNATE_ID, proposedAlternateId); - } else { - isAcceptable = canApplyAlternateId(cmHandleId, proposedAlternateId); - } - } - } - if (isAcceptable) { + if (isProposedAlternateIdAcceptable(proposedAlternateId, operation, acceptedAlternateIds, cmHandleId)) { acceptedAlternateIds.add(proposedAlternateId); } else { rejectedCmHandleIds.add(cmHandleId); @@ -140,6 +124,22 @@ public class AlternateIdChecker { return rejectedCmHandleIds; } + private boolean isProposedAlternateIdAcceptable(final String proposedAlternateId, final Operation operation, + final Set<String> acceptedAlternateIds, final String cmHandleId) { + if (StringUtils.isEmpty(proposedAlternateId)) { + return true; + } + if (acceptedAlternateIds.contains(proposedAlternateId)) { + log.warn("Alternate id update ignored, cannot update cm handle {}, alternate id is already " + + "assigned to a different cm handle (in this batch)", cmHandleId); + return false; + } + if (Operation.CREATE.equals(operation)) { + return canApplyAlternateId(cmHandleId, NO_CURRENT_ALTERNATE_ID, proposedAlternateId); + } + return canApplyAlternateId(cmHandleId, proposedAlternateId); + } + private boolean alternateIdAlreadyInDb(final String alternateId) { try { inventoryPersistence.getCmHandleDataNodeByAlternateId(alternateId); diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmResourceAddress.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmResourceAddress.java new file mode 100644 index 0000000000..21d82fcf56 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmResourceAddress.java @@ -0,0 +1,25 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.models; + +public record CmResourceAddress(String datastoreName, String cmHandleId, String resourceIdentifier) { + +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy index 74016e4c0c..d47be6cd5c 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy @@ -24,6 +24,7 @@ package org.onap.cps.ncmp.api.impl import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse +import org.onap.cps.ncmp.api.models.CmResourceAddress import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME @@ -121,35 +122,27 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { >> { new ResponseEntity<>(HttpStatus.CREATED) } } - def 'Get resource data for pass-through operational from DMI.'() { + def 'Get resource data for from DMI.'() { given: 'cpsDataService returns valid data node' mockDataNode() + and: 'some cm resource address' + def cmResourceAddress = new CmResourceAddress('some datastore','some CM Handle', 'some resource Id') and: 'get resource data from DMI is called' - mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_OPERATIONAL.datastoreName,'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) >> + mockDmiDataOperations.getResourceDataFromDmi(cmResourceAddress, OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) >> new ResponseEntity<>('dmi-response', HttpStatus.OK) - when: 'get resource data operational for cm-handle is called' - def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) + when: 'get resource data operational for the given cm resource address is called' + def response = objectUnderTest.getResourceDataForCmHandle(cmResourceAddress, OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) then: 'DMI returns a json response' assert response == 'dmi-response' } - def 'Get resource data for pass-through running from DMI.'() { - given: 'cpsDataService returns valid data node' - mockDataNode() - and: 'DMI returns valid response and data' - mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) >> - new ResponseEntity<>('{dmi-response}', HttpStatus.OK) - when: 'get resource data is called' - def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) - then: 'get resource data returns expected response' - assert response == '{dmi-response}' - } - def 'Get resource data for operational (cached) data.'() { given: 'CPS Data service returns some object(s)' mockCpsDataService.getDataNodes(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', FetchDescendantsOption.OMIT_DESCENDANTS) >> ['First Object', 'other Object'] + and: 'a cm resource address for the same datastore, cm handle and resource id' + def cmResourceAddress = new CmResourceAddress(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId') when: 'get resource data is called' - def response = objectUnderTest.getResourceDataForCmHandle(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', FetchDescendantsOption.OMIT_DESCENDANTS) + def response = objectUnderTest.getResourceDataForCmHandle(cmResourceAddress, FetchDescendantsOption.OMIT_DESCENDANTS) then: 'get resource data returns teh first object from the data service' assert response == 'First Object' } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImplSpec.groovy index eb0e1100ed..19ebc3d711 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImplSpec.groovy @@ -20,18 +20,22 @@ package org.onap.cps.ncmp.api.impl.events.cmsubscription.service - +import org.onap.cps.api.CpsDataService import org.onap.cps.api.CpsQueryService import org.onap.cps.ncmp.api.impl.operations.DatastoreType import org.onap.cps.spi.FetchDescendantsOption import org.onap.cps.spi.model.DataNode +import org.onap.cps.utils.JsonObjectMapper +import com.fasterxml.jackson.databind.ObjectMapper import spock.lang.Specification class CmNotificationSubscriptionPersistenceServiceImplSpec extends Specification { + def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) def mockCpsQueryService = Mock(CpsQueryService) + def mockCpsDataService = Mock(CpsDataService) - def objectUnderTest = new CmNotificationSubscriptionPersistenceServiceImpl(mockCpsQueryService) + def objectUnderTest = new CmNotificationSubscriptionPersistenceServiceImpl(jsonObjectMapper, mockCpsQueryService, mockCpsDataService) def 'Check ongoing cm subscription #scenario'() { given: 'a valid cm subscription query' @@ -64,4 +68,43 @@ class CmNotificationSubscriptionPersistenceServiceImplSpec extends Specification 'datanodes present' | [new DataNode()] || false 'no datanodes present' | [] || true } + + def 'Add new subscriber to an ongoing cm notification subscription'() { + given: 'a valid cm subscription path query' + def cpsPathQuery = objectUnderTest.CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted('ncmp-datastore:passthrough-running', 'ch-1', '/x/y'); + and: 'a dataNode exists for the given cps path query' + mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions', + cpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS) >> [new DataNode(xpath: cpsPathQuery, leaves: ['xpath': '/x/y','subscriptionIds': ['sub-1']])] + when: 'the method to add/update cm notification subscription is called' + objectUnderTest.addOrUpdateCmNotificationSubscription(DatastoreType.PASSTHROUGH_RUNNING, 'ch-1','/x/y', 'newSubId') + then: 'data service method to update list of subscribers is called once' + 1 * mockCpsDataService.updateNodeLeaves( + 'NCMP-Admin', + 'cm-data-subscriptions', + '/datastores/datastore[@name=\'ncmp-datastore:passthrough-running\']/cm-handles/cm-handle[@id=\'ch-1\']/filters', + '{"filter":[{"xpath":"/x/y","subscriptionIds":["sub-1","newSubId"]}]}', _) + } + + def 'Add new cm notification subscription for #datastoreType'() { + given: 'a valid cm subscription path query' + def cpsPathQuery = objectUnderTest.CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreName, 'ch-1', '/x/y') + and: 'a parent node xpath for given path above' + def parentNodeXpath = '/datastores/datastore[@name=\'%s\']/cm-handles' + and: 'a datanode does not exist for the given cps path query' + mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions', + cpsPathQuery.formatted(datastoreName), + FetchDescendantsOption.OMIT_DESCENDANTS) >> [] + when: 'the method to add/update cm notification subscription is called' + objectUnderTest.addOrUpdateCmNotificationSubscription(datastoreType, 'ch-1','/x/y', 'newSubId') + then: 'data service method to update list of subscribers is called once with the correct parameters' + 1 * mockCpsDataService.saveData( + 'NCMP-Admin', + 'cm-data-subscriptions', + parentNodeXpath.formatted(datastoreName), + '{"cm-handle":[{"id":"ch-1","filters":{"filter":[{"xpath":"/x/y","subscriptionIds":["newSubId"]}]}}]}', _,_) + where: + scenario | datastoreType || datastoreName + 'passthrough_running' | DatastoreType.PASSTHROUGH_RUNNING || "ncmp-datastore:passthrough-running" + 'passthrough_operational' | DatastoreType.PASSTHROUGH_OPERATIONAL || "ncmp-datastore:passthrough-operational" + } }
\ No newline at end of file diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy index e154588a10..eb6c7a0f48 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy @@ -28,6 +28,7 @@ import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder import org.onap.cps.ncmp.api.impl.utils.context.CpsApplicationContext import org.onap.cps.ncmp.api.models.DataOperationRequest +import org.onap.cps.ncmp.api.models.CmResourceAddress import org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent import org.onap.cps.ncmp.utils.TestUtils import org.onap.cps.utils.JsonObjectMapper @@ -81,8 +82,8 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { mockDmiRestClient.postOperationWithJsonData(expectedUrl, expectedJson, READ, NO_AUTH_HEADER) >> responseFromDmi dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl when: 'get resource data is invoked' - def result = objectUnderTest.getResourceDataFromDmi(dataStore.datastoreName, cmHandleId, resourceIdentifier, - options, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) + def cmResourceAddress = new CmResourceAddress(dataStore.datastoreName, cmHandleId, resourceIdentifier) + def result = objectUnderTest.getResourceDataFromDmi(cmResourceAddress, options, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) then: 'the result is the response from the DMI service' assert result == responseFromDmi where: 'the following parameters are used' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/AlternateIdCheckerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/AlternateIdCheckerSpec.groovy index 0eabaa1d28..1e843676be 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/AlternateIdCheckerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/AlternateIdCheckerSpec.groovy @@ -80,7 +80,8 @@ class AlternateIdCheckerSpec extends Specification { assert result == expectedRejectedCmHandleIds where: 'the following alternate ids are used' scenario | alt1 | alt2 | altAlreadyInDb || expectedRejectedCmHandleIds - 'no alternate ids' | '' | '' | ['dont matter'] || [] + 'blank alternate ids' | '' | '' | ['dont matter'] || [] + 'null alternate ids' | null | null | ['dont matter'] || [] 'new alternate ids' | 'fdn1' | 'fdn2' | ['other fdn'] || [] 'one already used alternate id' | 'fdn1' | 'fdn2' | ['fdn1'] || ['ch-1'] 'duplicate alternate id in batch' | 'fdn1' | 'fdn1' | ['dont matter'] || ['ch-2'] diff --git a/cps-parent/pom.xml b/cps-parent/pom.xml index 699bf3c9ac..b6e12c0082 100644 --- a/cps-parent/pom.xml +++ b/cps-parent/pom.xml @@ -159,7 +159,7 @@ <dependency> <groupId>com.github.spotbugs</groupId> <artifactId>spotbugs</artifactId> - <version>4.2.0</version> + <version>4.2.3</version> </dependency> <dependency> <groupId>${project.groupId}</groupId> diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index de427af13f..a604b06527 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -31,6 +31,8 @@ services: POSTGRES_DB: cpsdb POSTGRES_USER: ${DB_USERNAME:-cps} POSTGRES_PASSWORD: ${DB_PASSWORD:-cps} + volumes: + - ./postgres-init.sql:/docker-entrypoint-initdb.d/postgres-init.sql deploy: resources: reservations: diff --git a/docker-compose/postgres-init.sql b/docker-compose/postgres-init.sql new file mode 100644 index 0000000000..0c96de5b55 --- /dev/null +++ b/docker-compose/postgres-init.sql @@ -0,0 +1 @@ +ALTER SYSTEM SET shared_buffers = '512MB'; diff --git a/docs/deployment.rst b/docs/deployment.rst index de276ce28f..ba8fcd9347 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -12,6 +12,13 @@ CPS Deployment .. contents:: :depth: 2 +Database configuration +====================== +CPS uses PostgreSQL database. As per the `PostgreSQL documentation on resource consumption +<https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS>`_, the *shared_buffers* +parameter should be set between 25% and 40% of total memory. It has a default value of 128 megabytes, so this should be +set appropriately. For example, given a database with 2GB of memory, 512MB is a recommended value. + CPS OOM Charts ============== The CPS kubernetes chart is located in the `OOM repository <https://github.com/onap/oom/tree/master/kubernetes/cps>`_. diff --git a/integration-test/pom.xml b/integration-test/pom.xml index b379e9ff19..ca2b26d1c9 100644 --- a/integration-test/pom.xml +++ b/integration-test/pom.xml @@ -68,6 +68,11 @@ <scope>test</scope> </dependency> <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-web</artifactId> + <scope>test</scope> + </dependency> + <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-spring</artifactId> <scope>test</scope> @@ -78,6 +83,11 @@ <scope>test</scope> </dependency> <dependency> + <groupId>org.springframework.kafka</groupId> + <artifactId>spring-kafka-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <scope>test</scope> @@ -88,8 +98,8 @@ <scope>test</scope> </dependency> <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-web</artifactId> + <groupId>org.testcontainers</groupId> + <artifactId>kafka</artifactId> <scope>test</scope> </dependency> </dependencies> diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy index 33945a6c21..2603c48edf 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy @@ -21,12 +21,14 @@ package org.onap.cps.integration.base import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.CpsDataService import org.onap.cps.api.CpsDataspaceService import org.onap.cps.api.CpsModuleService import org.onap.cps.api.CpsQueryService import org.onap.cps.integration.DatabaseTestContainer +import org.onap.cps.integration.KafkaTestContainer import org.onap.cps.ncmp.api.NetworkCmProxyCmHandleQueryService import org.onap.cps.ncmp.api.NetworkCmProxyDataService import org.onap.cps.ncmp.api.NetworkCmProxyQueryService @@ -38,6 +40,7 @@ import org.onap.cps.spi.exceptions.DataspaceNotFoundException import org.onap.cps.spi.model.DataNode import org.onap.cps.spi.repository.DataspaceRepository import org.onap.cps.spi.utils.SessionManager +import org.onap.cps.utils.JsonObjectMapper import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.EnableAutoConfiguration import org.springframework.boot.autoconfigure.domain.EntityScan @@ -55,13 +58,11 @@ import spock.lang.Shared import spock.lang.Specification import spock.util.concurrent.PollingConditions -import java.time.format.DateTimeFormatter - +import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME +import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR +import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus -import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME; -import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR; -import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = [CpsDataspaceService]) @Testcontainers @@ -75,8 +76,11 @@ abstract class CpsIntegrationSpecBase extends Specification { @Shared DatabaseTestContainer databaseTestContainer = DatabaseTestContainer.getInstance() + @Shared + KafkaTestContainer kafkaTestContainer = KafkaTestContainer.getInstance() + @Autowired - MockMvc mvc; + MockMvc mvc @Autowired CpsDataspaceService cpsDataspaceService @@ -111,6 +115,9 @@ abstract class CpsIntegrationSpecBase extends Specification { @Autowired ModuleSyncWatchdog moduleSyncWatchdog + @Autowired + JsonObjectMapper jsonObjectMapper + MockRestServiceServer mockDmiServer = null static final DMI_URL = 'http://mock-dmi-server' diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmHandleCreateSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmHandleCreateSpec.groovy index 6b6f62edf9..f03872d56b 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmHandleCreateSpec.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmHandleCreateSpec.groovy @@ -20,7 +20,11 @@ package org.onap.cps.integration.functional +import java.time.Duration import java.time.OffsetDateTime +import org.apache.kafka.common.TopicPartition +import org.apache.kafka.common.serialization.StringDeserializer +import org.onap.cps.integration.KafkaTestContainer import org.onap.cps.integration.base.CpsIntegrationSpecBase import org.onap.cps.ncmp.api.NetworkCmProxyDataService import org.onap.cps.ncmp.api.impl.inventory.CmHandleState @@ -28,12 +32,15 @@ import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse import org.onap.cps.ncmp.api.models.DmiPluginRegistration import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle +import org.onap.cps.ncmp.events.lcm.v1.LcmEvent import spock.util.concurrent.PollingConditions class NcmpCmHandleCreateSpec extends CpsIntegrationSpecBase { NetworkCmProxyDataService objectUnderTest + def kafkaConsumer = KafkaTestContainer.getConsumer('ncmp-group', StringDeserializer.class) + static final MODULE_REFERENCES_RESPONSE_A = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_Response.json') static final MODULE_RESOURCES_RESPONSE_A = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_ResourcesResponse.json') static final MODULE_REFERENCES_RESPONSE_B = readResourceDataFile('mock-dmi-responses/bookStoreBWithModules_M1_M3_Response.json') @@ -47,6 +54,9 @@ class NcmpCmHandleCreateSpec extends CpsIntegrationSpecBase { given: 'DMI will return modules when requested' mockDmiResponsesForModuleSync(DMI_URL, 'ch-1', MODULE_REFERENCES_RESPONSE_A, MODULE_RESOURCES_RESPONSE_A) + and: 'consumer subscribed to topic' + kafkaConsumer.subscribe(['ncmp-events']) + when: 'a CM-handle is registered for creation' def cmHandleToCreate = new NcmpServiceCmHandle(cmHandleId: 'ch-1') def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: DMI_URL, createdCmHandles: [cmHandleToCreate]) @@ -66,6 +76,14 @@ class NcmpCmHandleCreateSpec extends CpsIntegrationSpecBase { assert CmHandleState.READY == objectUnderTest.getCmHandleCompositeState('ch-1').cmHandleState }) + and: 'the messages is polled' + def message = kafkaConsumer.poll(Duration.ofMillis(10000)) + def records = message.records(new TopicPartition('ncmp-events', 0)) + + and: 'the newest lcm event notification is received with READY state' + def notificationMessage = jsonObjectMapper.convertJsonString(records.last().value().toString(), LcmEvent) + assert notificationMessage.event.newValues.cmHandleState.value() == 'READY' + and: 'the CM-handle has expected modules' assert ['M1', 'M2'] == objectUnderTest.getYangResourcesModuleReferences('ch-1').moduleName.sort() diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmNotificationSubscriptionSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmNotificationSubscriptionSpec.groovy new file mode 100644 index 0000000000..df74a05b50 --- /dev/null +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmNotificationSubscriptionSpec.groovy @@ -0,0 +1,46 @@ +package org.onap.cps.integration.functional + +import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING; +import org.onap.cps.integration.base.CpsIntegrationSpecBase; +import org.onap.cps.ncmp.api.impl.events.cmsubscription.service.CmNotificationSubscriptionPersistenceService; +import org.springframework.beans.factory.annotation.Autowired; + +class NcmpCmNotificationSubscriptionSpec extends CpsIntegrationSpecBase { + + @Autowired + CmNotificationSubscriptionPersistenceService cmNotificationSubscriptionPersistenceService; + + def 'Adding a new cm notification subscription'() { + given: 'there is no ongoing cm subscription for the following' + def datastoreType = PASSTHROUGH_RUNNING + def cmHandleId = 'ch-1' + def xpath = '/x/y' + assert cmNotificationSubscriptionPersistenceService. + getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size() == 0 + when: 'we add a new cm notification subscription' + cmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(datastoreType,cmHandleId,xpath, + 'subId-1') + then: 'there is an ongoing cm subscription for that CM handle and xpath' + assert cmNotificationSubscriptionPersistenceService.isOngoingCmNotificationSubscription(datastoreType,cmHandleId,xpath) + and: 'only one subscription id is related to now ongoing cm subscription' + assert cmNotificationSubscriptionPersistenceService. + getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size() == 1 + } + + def 'Adding a cm notification subscription to an already existing'() { + given: 'an ongoing cm subscription' + def datastoreType = PASSTHROUGH_RUNNING + def cmHandleId = 'ch-1' + def xpath = '/x/y' + cmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(datastoreType,cmHandleId,xpath, + 'subId-1') + when: 'a new cm notification subscription is made for the SAME CM handle and xpath' + cmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(datastoreType,cmHandleId,xpath, + 'subId-2') + then: 'it is added to the ongoing list of subscription ids' + def subscriptionIds = cmNotificationSubscriptionPersistenceService.getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath) + assert subscriptionIds.size() == 2 + and: 'both subscription ids exists for the CM handle and xpath' + assert subscriptionIds.contains("subId-1") && subscriptionIds.contains("subId-2") + } +} diff --git a/integration-test/src/test/java/org/onap/cps/integration/KafkaTestContainer.java b/integration-test/src/test/java/org/onap/cps/integration/KafkaTestContainer.java new file mode 100644 index 0000000000..d41f752912 --- /dev/null +++ b/integration-test/src/test/java/org/onap/cps/integration/KafkaTestContainer.java @@ -0,0 +1,86 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.integration; + +import java.util.HashMap; +import java.util.Map; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.utility.DockerImageName; + +/** + * The Apache Kafka test container wrapper. + * Allow to use specific image and version with Singleton design pattern. + * This ensures only one instance of Kafka container across the integration tests. + * Avoid unnecessary resource and time consumption. + */ +public class KafkaTestContainer extends KafkaContainer { + + private static final String IMAGE_NAME_AND_VERSION = "registry.nordix.org/onaptest/confluentinc/cp-kafka:6.2.1"; + + private static KafkaTestContainer kafkaTestContainer; + + private KafkaTestContainer() { + super(DockerImageName.parse(IMAGE_NAME_AND_VERSION).asCompatibleSubstituteFor("confluentinc/cp-kafka")); + } + + /** + * Provides an instance of Kafka test container wrapper. + * This will allow to initialize Kafka messaging support before any integration test run. + * + * @return KafkaTestContainer the unique Kafka instance + */ + public static KafkaTestContainer getInstance() { + if (kafkaTestContainer == null) { + kafkaTestContainer = new KafkaTestContainer(); + Runtime.getRuntime().addShutdownHook(new Thread(kafkaTestContainer::close)); + } + return kafkaTestContainer; + } + + public static KafkaConsumer getConsumer(final String consumerGroupId, final Object valueDeserializer) { + return new KafkaConsumer<>(consumerProperties(consumerGroupId, valueDeserializer)); + } + + @Override + public void start() { + super.start(); + System.setProperty("spring.kafka.properties.bootstrap.servers", kafkaTestContainer.getBootstrapServers()); + } + + @Override + public void stop() { + // Method intentionally left blank + } + + private static Map<String, Object> consumerProperties(final String consumerGroupId, + final Object valueDeserializer) { + final Map<String, Object> configProps = new HashMap<>(); + configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaTestContainer.getBootstrapServers()); + configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, valueDeserializer); + configProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + configProps.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroupId); + return configProps; + } + +} |