diff options
author | lukegleeson <luke.gleeson@est.tech> | 2022-07-11 10:55:53 +0100 |
---|---|---|
committer | lukegleeson <luke.gleeson@est.tech> | 2022-07-29 10:42:04 +0100 |
commit | 82a550f6b080cb50912d93f7b13ba0fc97a95470 (patch) | |
tree | 1bb3ca97c99e46c9b5a5654b2848038c37f20c4d /cps-ncmp-service | |
parent | 054873c7c52bdb9fae718a0d7651d57b1a995dfc (diff) |
Query CmHandles using CPS path
Added withCpsPath condition parameter
Validated to prevent misuse and blocking of querying using private properties
Updated OpenAPI with examples and links to documentation
Moved methods related to cmHandle querying using cps path from InventoryPersistence to CmHandleQueries
Renamed private method deleteSchemaSetAndListElementByCmHandleId to deleteCmHandleByCmHandleId
Issue-ID: CPS-977
Change-Id: I83827215b7e58de74f8f62cd0140516d217d93f1
Signed-off-by: lukegleeson <luke.gleeson@est.tech>
Diffstat (limited to 'cps-ncmp-service')
10 files changed, 527 insertions, 305 deletions
diff --git a/cps-ncmp-service/pom.xml b/cps-ncmp-service/pom.xml index 93c265a7bf..d94c6d1dec 100644 --- a/cps-ncmp-service/pom.xml +++ b/cps-ncmp-service/pom.xml @@ -3,7 +3,7 @@ ============LICENSE_START======================================================= Copyright (C) 2021-2022 Nordix Foundation Modifications Copyright (C) 2021 Pantheon.tech - Modifications Copyright (C) 2022 Bell Canada + Modifications Copyright (C) 2022 Bell Canada ================================================================================ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -46,6 +46,10 @@ <artifactId>cps-ncmp-events</artifactId> </dependency> <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>cps-path-parser</artifactId> + </dependency> + <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> </dependency> diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java index a62a009ce7..d784bcdeac 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java @@ -21,6 +21,8 @@ package org.onap.cps.ncmp.api.impl; import static org.onap.cps.ncmp.api.impl.utils.YangDataConverter.convertYangModelCmHandleToNcmpServiceCmHandle; +import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS; +import static org.onap.cps.utils.CmHandleQueryRestParametersValidator.validateCpsPathConditionProperties; import static org.onap.cps.utils.CmHandleQueryRestParametersValidator.validateModuleNameConditionProperties; import java.util.ArrayList; @@ -31,17 +33,22 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.onap.cps.cpspath.parser.PathParsingException; import org.onap.cps.ncmp.api.NetworkCmProxyCmHandlerQueryService; import org.onap.cps.ncmp.api.impl.utils.YangDataConverter; +import org.onap.cps.ncmp.api.inventory.CmHandleQueries; import org.onap.cps.ncmp.api.inventory.InventoryPersistence; import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; +import org.onap.cps.spi.exceptions.DataValidationException; import org.onap.cps.spi.model.Anchor; import org.onap.cps.spi.model.CmHandleQueryServiceParameters; import org.onap.cps.spi.model.ConditionProperties; import org.onap.cps.spi.model.DataNode; +import org.onap.cps.utils.ValidQueryProperties; import org.springframework.stereotype.Service; @Service @@ -49,9 +56,8 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCmHandlerQueryService { - private static final String PROPERTY_QUERY_NAME = "hasAllProperties"; - private static final String MODULE_QUERY_NAME = "hasAllModules"; - private static final Map<String, NcmpServiceCmHandle> NO_QUERY_EXECUTED = null; + private static final Map<String, NcmpServiceCmHandle> NO_QUERY_TO_EXECUTE = null; + private final CmHandleQueries cmHandleQueries; private final InventoryPersistence inventoryPersistence; /** @@ -68,14 +74,10 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm return getAllCmHandles(); } - final Map<String, NcmpServiceCmHandle> publicPropertyQueryResult - = executePublicPropertyQueries(cmHandleQueryServiceParameters); + final Map<String, NcmpServiceCmHandle> combinedQueryResult = executeInventoryQueries( + cmHandleQueryServiceParameters); - final Map<String, NcmpServiceCmHandle> combinedQueryResult = - combineWithModuleNameQuery(cmHandleQueryServiceParameters, publicPropertyQueryResult); - - return combinedQueryResult == NO_QUERY_EXECUTED - ? Collections.emptySet() : new HashSet<>(combinedQueryResult.values()); + return new HashSet<>(combineWithModuleNameQuery(cmHandleQueryServiceParameters, combinedQueryResult).values()); } /** @@ -92,52 +94,24 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm return getAllCmHandleIds(); } - final Map<String, NcmpServiceCmHandle> publicPropertyQueryResult - = executePublicPropertyQueries(cmHandleQueryServiceParameters); + final Map<String, NcmpServiceCmHandle> combinedQueryResult = executeInventoryQueries( + cmHandleQueryServiceParameters); final Collection<String> moduleNamesForQuery = getModuleNamesForQuery(cmHandleQueryServiceParameters.getCmHandleQueryParameters()); if (moduleNamesForQuery.isEmpty()) { - return publicPropertyQueryResult == NO_QUERY_EXECUTED - ? Collections.emptySet() : publicPropertyQueryResult.keySet(); + return combinedQueryResult.keySet(); } final Set<String> moduleNameQueryResult = getNamesOfAnchorsWithGivenModules(moduleNamesForQuery); - if (publicPropertyQueryResult == NO_QUERY_EXECUTED) { + if (combinedQueryResult == NO_QUERY_TO_EXECUTE) { return moduleNameQueryResult; } - moduleNameQueryResult.retainAll(publicPropertyQueryResult.keySet()); + moduleNameQueryResult.retainAll(combinedQueryResult.keySet()); return moduleNameQueryResult; } - private Map<String, NcmpServiceCmHandle> executePublicPropertyQueries( - final CmHandleQueryServiceParameters cmHandleQueryServiceParameters) { - final Map<String, String> publicPropertyQueryPairs = - getPublicPropertyPairs(cmHandleQueryServiceParameters.getCmHandleQueryParameters()); - if (publicPropertyQueryPairs.isEmpty()) { - return NO_QUERY_EXECUTED; - } - Map<String, NcmpServiceCmHandle> cmHandleIdToNcmpServiceCmHandles = null; - for (final Map.Entry<String, String> entry : publicPropertyQueryPairs.entrySet()) { - final String cpsPath = "//public-properties[@name='" + entry.getKey() + "' and @value='" - + entry.getValue() + "']/ancestor::cm-handles"; - - final Collection<DataNode> dataNodes = inventoryPersistence.queryDataNodes(cpsPath); - if (cmHandleIdToNcmpServiceCmHandles == NO_QUERY_EXECUTED) { - cmHandleIdToNcmpServiceCmHandles = collectDataNodesToNcmpServiceCmHandles(dataNodes); - } else { - final Collection<String> cmHandleIdsToRetain = dataNodes.parallelStream() - .map(dataNode -> dataNode.getLeaves().get("id").toString()).collect(Collectors.toSet()); - cmHandleIdToNcmpServiceCmHandles.keySet().retainAll(cmHandleIdsToRetain); - } - if (cmHandleIdToNcmpServiceCmHandles.isEmpty()) { - break; - } - } - return cmHandleIdToNcmpServiceCmHandles; - } - private Map<String, NcmpServiceCmHandle> combineWithModuleNameQuery( final CmHandleQueryServiceParameters cmHandleQueryServiceParameters, final Map<String, NcmpServiceCmHandle> previousQueryResult) { @@ -151,7 +125,7 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm return Collections.emptyMap(); } final Map<String, NcmpServiceCmHandle> queryResult = new HashMap<>(cmHandleIdsByModuleName.size()); - if (previousQueryResult == NO_QUERY_EXECUTED) { + if (previousQueryResult == NO_QUERY_TO_EXECUTE) { cmHandleIdsByModuleName.forEach(cmHandleId -> queryResult.put(cmHandleId, createNcmpServiceCmHandle( inventoryPersistence.getDataNode("/dmi-registry/cm-handles[@id='" + cmHandleId + "']"))) @@ -163,19 +137,68 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm return queryResult; } + private Map<String, NcmpServiceCmHandle> executeInventoryQueries( + final CmHandleQueryServiceParameters cmHandleQueryServiceParameters) { + final Map<String, String> cpsPath = getCpsPath(cmHandleQueryServiceParameters.getCmHandleQueryParameters()); + if (!validateCpsPathConditionProperties(cpsPath)) { + return Collections.emptyMap(); + } + final Map<String, NcmpServiceCmHandle> cpsPathQueryResult; + if (cpsPath.isEmpty()) { + cpsPathQueryResult = NO_QUERY_TO_EXECUTE; + } else { + try { + cpsPathQueryResult = cmHandleQueries.getCmHandleDataNodesByCpsPath( + cpsPath.get("cpsPath"), INCLUDE_ALL_DESCENDANTS) + .stream().map(this::createNcmpServiceCmHandle) + .collect(Collectors.toMap(NcmpServiceCmHandle::getCmHandleId, + Function.identity())); + } catch (final PathParsingException pathParsingException) { + throw new DataValidationException(pathParsingException.getMessage(), pathParsingException.getDetails(), + pathParsingException); + } + if (cpsPathQueryResult.isEmpty()) { + return Collections.emptyMap(); + } + } + + final Map<String, String> publicPropertyQueryPairs = + getPublicPropertyPairs(cmHandleQueryServiceParameters.getCmHandleQueryParameters()); + final Map<String, NcmpServiceCmHandle> propertiesQueryResult = publicPropertyQueryPairs.isEmpty() + ? NO_QUERY_TO_EXECUTE : cmHandleQueries.queryCmHandlePublicProperties(publicPropertyQueryPairs); + + return cmHandleQueries.combineCmHandleQueries(cpsPathQueryResult, propertiesQueryResult); + } + private Set<String> getNamesOfAnchorsWithGivenModules(final Collection<String> moduleNamesForQuery) { final Collection<Anchor> anchors = inventoryPersistence.queryAnchors(moduleNamesForQuery); return anchors.parallelStream().map(Anchor::getName).collect(Collectors.toSet()); } - private Map<String, NcmpServiceCmHandle> collectDataNodesToNcmpServiceCmHandles( - final Collection<DataNode> dataNodes) { - final Map<String, NcmpServiceCmHandle> cmHandleIdToNcmpServiceCmHandle = new HashMap<>(); - dataNodes.forEach(dataNode -> { - final NcmpServiceCmHandle ncmpServiceCmHandle = createNcmpServiceCmHandle(dataNode); - cmHandleIdToNcmpServiceCmHandle.put(ncmpServiceCmHandle.getCmHandleId(), ncmpServiceCmHandle); - }); - return cmHandleIdToNcmpServiceCmHandle; + private Collection<String> getModuleNamesForQuery(final List<ConditionProperties> conditionProperties) { + final List<String> result = new ArrayList<>(); + getConditions(conditionProperties, ValidQueryProperties.HAS_ALL_MODULES.getQueryProperty()) + .parallelStream().forEach( + conditionProperty -> { + validateModuleNameConditionProperties(conditionProperty); + result.add(conditionProperty.get("moduleName")); + } + ); + return result; + } + + private Map<String, String> getCpsPath(final List<ConditionProperties> conditionProperties) { + final Map<String, String> result = new HashMap<>(); + getConditions(conditionProperties, ValidQueryProperties.WITH_CPS_PATH.getQueryProperty()).forEach( + result::putAll); + return result; + } + + private Map<String, String> getPublicPropertyPairs(final List<ConditionProperties> conditionProperties) { + final Map<String, String> result = new HashMap<>(); + getConditions(conditionProperties, + ValidQueryProperties.HAS_ALL_PROPERTIES.getQueryProperty()).forEach(result::putAll); + return result; } private List<Map<String, String>> getConditions(final List<ConditionProperties> conditionProperties, @@ -188,23 +211,6 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm return Collections.emptyList(); } - private Collection<String> getModuleNamesForQuery(final List<ConditionProperties> conditionProperties) { - final List<String> result = new ArrayList<>(); - getConditions(conditionProperties, MODULE_QUERY_NAME).parallelStream().forEach( - conditionProperty -> { - validateModuleNameConditionProperties(conditionProperty); - result.add(conditionProperty.get("moduleName")); - } - ); - return result; - } - - private Map<String, String> getPublicPropertyPairs(final List<ConditionProperties> conditionProperties) { - final Map<String, String> result = new HashMap<>(); - getConditions(conditionProperties, PROPERTY_QUERY_NAME).forEach(result::putAll); - return result; - } - private Set<NcmpServiceCmHandle> getAllCmHandles() { return inventoryPersistence.getDataNode("/dmi-registry") .getChildDataNodes().stream().map(this::createNcmpServiceCmHandle).collect(Collectors.toSet()); 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 8d32c1ade5..28cbf2cc24 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 @@ -271,7 +271,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(cmHandleId); lcmEventsCmHandleStateHandler.updateCmHandleState(yangModelCmHandle, CmHandleState.DELETING); - deleteSchemaSetAndListElementByCmHandleId(cmHandleId); + deleteCmHandleByCmHandleId(cmHandleId); cmHandleRegistrationResponses.add(CmHandleRegistrationResponse.createSuccessResponse(cmHandleId)); lcmEventsCmHandleStateHandler.updateCmHandleState(yangModelCmHandle, CmHandleState.DELETED); @@ -295,7 +295,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService return cmHandleRegistrationResponses; } - private void deleteSchemaSetAndListElementByCmHandleId(final String cmHandleId) { + private void deleteCmHandleByCmHandleId(final String cmHandleId) { inventoryPersistence.deleteSchemaSetWithCascade(cmHandleId); inventoryPersistence.deleteListOrListElement("/dmi-registry/cm-handles[@id='" + cmHandleId + "']"); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/CmHandleQueries.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/CmHandleQueries.java new file mode 100644 index 0000000000..92387bab32 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/CmHandleQueries.java @@ -0,0 +1,165 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 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.inventory; + +import static org.onap.cps.ncmp.api.impl.utils.YangDataConverter.convertYangModelCmHandleToNcmpServiceCmHandle; +import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.onap.cps.ncmp.api.impl.utils.YangDataConverter; +import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; +import org.onap.cps.spi.CpsDataPersistenceService; +import org.onap.cps.spi.FetchDescendantsOption; +import org.onap.cps.spi.model.DataNode; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class CmHandleQueries { + + private static final String NCMP_DATASPACE_NAME = "NCMP-Admin"; + private static final String NCMP_DMI_REGISTRY_ANCHOR = "ncmp-dmi-registry"; + + private final CpsDataPersistenceService cpsDataPersistenceService; + private static final Map<String, NcmpServiceCmHandle> NO_QUERY_TO_EXECUTE = null; + private static final String ANCESTOR_CM_HANDLES = "/ancestor::cm-handles"; + + + /** + * Query CmHandles based on PublicProperties. + * + * @param publicPropertyQueryPairs public properties for query + * @return CmHandles which have these public properties + */ + public Map<String, NcmpServiceCmHandle> queryCmHandlePublicProperties( + final Map<String, String> publicPropertyQueryPairs) { + if (publicPropertyQueryPairs.isEmpty()) { + return Collections.emptyMap(); + } + Map<String, NcmpServiceCmHandle> cmHandleIdToNcmpServiceCmHandles = null; + for (final Map.Entry<String, String> publicPropertyQueryPair : publicPropertyQueryPairs.entrySet()) { + final String cpsPath = "//public-properties[@name=\"" + publicPropertyQueryPair.getKey() + + "\" and @value=\"" + publicPropertyQueryPair.getValue() + "\"]"; + + final Collection<DataNode> dataNodes = getCmHandleDataNodesByCpsPath(cpsPath, INCLUDE_ALL_DESCENDANTS); + if (cmHandleIdToNcmpServiceCmHandles == null) { + cmHandleIdToNcmpServiceCmHandles = collectDataNodesToNcmpServiceCmHandles(dataNodes); + } else { + final Collection<String> cmHandleIdsToRetain = dataNodes.parallelStream() + .map(dataNode -> dataNode.getLeaves().get("id").toString()).collect(Collectors.toSet()); + cmHandleIdToNcmpServiceCmHandles.keySet().retainAll(cmHandleIdsToRetain); + } + if (cmHandleIdToNcmpServiceCmHandles.isEmpty()) { + break; + } + } + return cmHandleIdToNcmpServiceCmHandles; + } + + /** + * Combine Maps of CmHandles. + * + * @param firstQuery first CmHandles Map + * @param secondQuery second CmHandles Map + * @return combined Map of CmHandles + */ + public Map<String, NcmpServiceCmHandle> combineCmHandleQueries( + final Map<String, NcmpServiceCmHandle> firstQuery, + final Map<String, NcmpServiceCmHandle> secondQuery) { + if (firstQuery == NO_QUERY_TO_EXECUTE && secondQuery == NO_QUERY_TO_EXECUTE) { + return Collections.emptyMap(); + } else if (firstQuery == NO_QUERY_TO_EXECUTE) { + return secondQuery; + } else if (secondQuery == NO_QUERY_TO_EXECUTE) { + return firstQuery; + } else { + firstQuery.keySet().retainAll(secondQuery.keySet()); + return firstQuery; + } + } + + /** + * Method which returns cm handles by the cm handles state. + * + * @param cmHandleState cm handle state + * @return a list of cm handles + */ + public List<DataNode> getCmHandlesByState(final CmHandleState cmHandleState) { + return getCmHandleDataNodesByCpsPath("//state[@cm-handle-state=\"" + cmHandleState + "\"]", + INCLUDE_ALL_DESCENDANTS); + } + + /** + * Method to return data nodes representing the cm handles. + * + * @param cpsPath cps path for which the cmHandle is requested + * @return a list of data nodes representing the cm handles. + */ + public List<DataNode> getCmHandleDataNodesByCpsPath(final String cpsPath, + final FetchDescendantsOption fetchDescendantsOption) { + return cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + cpsPath + ANCESTOR_CM_HANDLES, fetchDescendantsOption); + } + + /** + * Method which returns cm handles by the cm handle id and state. + * @param cmHandleId cm handle id + * @param cmHandleState cm handle state + * @return a list of cm handles + */ + public List<DataNode> getCmHandlesByIdAndState(final String cmHandleId, final CmHandleState cmHandleState) { + return getCmHandleDataNodesByCpsPath("//cm-handles[@id='" + cmHandleId + "']/state[@cm-handle-state=\"" + + cmHandleState + "\"]", FetchDescendantsOption.OMIT_DESCENDANTS); + } + + /** + * Method which returns cm handles by the operational sync state of cm handle. + * @param dataStoreSyncState sync state + * @return a list of cm handles + */ + public List<DataNode> getCmHandlesByOperationalSyncState(final DataStoreSyncState dataStoreSyncState) { + return getCmHandleDataNodesByCpsPath("//state/datastores" + "/operational[@sync-state=\"" + + dataStoreSyncState + "\"]", FetchDescendantsOption.OMIT_DESCENDANTS); + } + + private Map<String, NcmpServiceCmHandle> collectDataNodesToNcmpServiceCmHandles( + final Collection<DataNode> dataNodes) { + final Map<String, NcmpServiceCmHandle> cmHandleIdToNcmpServiceCmHandle = new HashMap<>(); + dataNodes.forEach(dataNode -> { + final NcmpServiceCmHandle ncmpServiceCmHandle = createNcmpServiceCmHandle(dataNode); + cmHandleIdToNcmpServiceCmHandle.put(ncmpServiceCmHandle.getCmHandleId(), ncmpServiceCmHandle); + }); + return cmHandleIdToNcmpServiceCmHandle; + } + + private NcmpServiceCmHandle createNcmpServiceCmHandle(final DataNode dataNode) { + return convertYangModelCmHandleToNcmpServiceCmHandle(YangDataConverter + .convertCmHandleToYangModel(dataNode, dataNode.getLeaves().get("id").toString())); + } +} + + diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java index be26a58d58..14fc6d698a 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java @@ -28,7 +28,6 @@ import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS; import java.time.OffsetDateTime; import java.util.Collection; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.api.CpsDataService; @@ -60,8 +59,6 @@ public class InventoryPersistence { private static final String CM_HANDLE_XPATH_TEMPLATE = "/dmi-registry/cm-handles[@id='" + "%s" + "']"; - private static final String ANCESTOR_CM_HANDLES = "\"]/ancestor::cm-handles"; - private final JsonObjectMapper jsonObjectMapper; private final CpsDataService cpsDataService; @@ -100,57 +97,6 @@ public class InventoryPersistence { } /** - * Method which returns cm handles by the cm handles state. - * - * @param cmHandleState cm handle state - * @return a list of cm handles - */ - public List<DataNode> getCmHandlesByState(final CmHandleState cmHandleState) { - return cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME, - NCMP_DMI_REGISTRY_ANCHOR, "//state[@cm-handle-state=\"" - + cmHandleState + ANCESTOR_CM_HANDLES, - FetchDescendantsOption.OMIT_DESCENDANTS); - } - - /** - * Method to return data nodes representing the cm handles. - * - * @param cpsPath cps path for which the cmHandle is requested - * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes - * @return a list of data nodes representing the cm handles. - */ - public List<DataNode> getCmHandleDataNodesByCpsPath(final String cpsPath, - final FetchDescendantsOption fetchDescendantsOption) { - return cpsDataPersistenceService.queryDataNodes( - NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cpsPath, fetchDescendantsOption); - } - - /** - * Method which returns cm handles by the cm handle id and state. - * @param cmHandleId cm handle id - * @param cmHandleState cm handle state - * @return a list of cm handles - */ - public List<DataNode> getCmHandlesByIdAndState(final String cmHandleId, final CmHandleState cmHandleState) { - return cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME, - NCMP_DMI_REGISTRY_ANCHOR, "//cm-handles[@id='" + cmHandleId + "']/state[@cm-handle-state=\"" - + cmHandleState + ANCESTOR_CM_HANDLES, - FetchDescendantsOption.OMIT_DESCENDANTS); - } - - /** - * Method which returns cm handles by the operational sync state of cm handle. - * @param dataStoreSyncState sync state - * @return a list of cm handles - */ - public List<DataNode> getCmHandlesByOperationalSyncState(final DataStoreSyncState dataStoreSyncState) { - return cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME, - NCMP_DMI_REGISTRY_ANCHOR, "//state/datastores" - + "/operational[@sync-state=\"" + dataStoreSyncState + ANCESTOR_CM_HANDLES, - FetchDescendantsOption.OMIT_DESCENDANTS); - } - - /** * This method retrieves DMI service name, DMI properties and the state for a given cm handle. * @param cmHandleId the id of the cm handle * @return yang model cm handle @@ -219,17 +165,6 @@ public class InventoryPersistence { } /** - * Query data nodes via cps path. - * - * @param cpsPath cps path - * @return List of data nodes - */ - public List<DataNode> queryDataNodes(final String cpsPath) { - return cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - cpsPath, INCLUDE_ALL_DESCENDANTS); - } - - /** * Get data node via xpath. * * @param xpath xpath diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/SyncUtils.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/SyncUtils.java index 467fd8f608..2b7d3c99cb 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/SyncUtils.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/SyncUtils.java @@ -40,6 +40,7 @@ import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations; import org.onap.cps.ncmp.api.impl.operations.DmiOperations; import org.onap.cps.ncmp.api.impl.utils.YangDataConverter; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; +import org.onap.cps.ncmp.api.inventory.CmHandleQueries; import org.onap.cps.ncmp.api.inventory.CmHandleState; import org.onap.cps.ncmp.api.inventory.CompositeState; import org.onap.cps.ncmp.api.inventory.DataStoreSyncState; @@ -57,6 +58,8 @@ import org.springframework.stereotype.Service; public class SyncUtils { private final InventoryPersistence inventoryPersistence; + private final CmHandleQueries cmHandleQueries; + private final DmiDataOperations dmiDataOperations; private final JsonObjectMapper jsonObjectMapper; @@ -70,7 +73,7 @@ public class SyncUtils { */ public List<YangModelCmHandle> getAdvisedCmHandles() { final List<DataNode> advisedCmHandlesAsDataNodeList = new ArrayList<>( - inventoryPersistence.getCmHandlesByState(CmHandleState.ADVISED)); + cmHandleQueries.getCmHandlesByState(CmHandleState.ADVISED)); log.info("Total number of fetched advised cm handle(s) is (are) {}", advisedCmHandlesAsDataNodeList.size()); if (advisedCmHandlesAsDataNodeList.isEmpty()) { return Collections.emptyList(); @@ -87,16 +90,16 @@ public class SyncUtils { * return null if not found */ public YangModelCmHandle getAnUnSynchronizedReadyCmHandle() { - final List<DataNode> unSynchronizedCmHandles = inventoryPersistence - .getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED); + final List<DataNode> unSynchronizedCmHandles = cmHandleQueries + .getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED); if (unSynchronizedCmHandles.isEmpty()) { return null; } Collections.shuffle(unSynchronizedCmHandles); for (final DataNode cmHandle : unSynchronizedCmHandles) { final String cmHandleId = cmHandle.getLeaves().get("id").toString(); - final List<DataNode> readyCmHandles = inventoryPersistence - .getCmHandlesByIdAndState(cmHandleId, CmHandleState.READY); + final List<DataNode> readyCmHandles = cmHandleQueries + .getCmHandlesByIdAndState(cmHandleId, CmHandleState.READY); if (!readyCmHandles.isEmpty()) { return inventoryPersistence.getYangModelCmHandle(cmHandleId); } @@ -110,8 +113,8 @@ public class SyncUtils { * @return a random LOCKED yang model cm handle, return null if not found */ public List<YangModelCmHandle> getModuleSyncFailedCmHandles() { - final List<DataNode> lockedCmHandlesAsDataNodeList = inventoryPersistence.getCmHandleDataNodesByCpsPath( - "//lock-reason[@reason=\"LOCKED_MODULE_SYNC_FAILED\"]/ancestor::cm-handles", + final List<DataNode> lockedCmHandlesAsDataNodeList = cmHandleQueries.getCmHandleDataNodesByCpsPath( + "//lock-reason[@reason=\"LOCKED_MODULE_SYNC_FAILED\"]", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS); return convertCmHandlesDataNodesToYangModelCmHandles(lockedCmHandlesAsDataNodeList); } @@ -171,8 +174,8 @@ public class SyncUtils { */ public String getResourceData(final String cmHandleId) { final ResponseEntity<Object> resourceDataResponseEntity = dmiDataOperations.getResourceDataFromDmi( - cmHandleId, DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, - UUID.randomUUID().toString()); + cmHandleId, DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, + UUID.randomUUID().toString()); if (resourceDataResponseEntity.getStatusCode().is2xxSuccessful()) { return getFirstResource(resourceDataResponseEntity.getBody()); } @@ -188,8 +191,8 @@ public class SyncUtils { } private List<YangModelCmHandle> convertCmHandlesDataNodesToYangModelCmHandles( - final List<DataNode> cmHandlesAsDataNodeList) { + final List<DataNode> cmHandlesAsDataNodeList) { return cmHandlesAsDataNodeList.stream().map(dataNode -> YangDataConverter.convertCmHandleToYangModel(dataNode, - dataNode.getLeaves().get("id").toString())).collect(Collectors.toList()); + dataNode.getLeaves().get("id").toString())).collect(Collectors.toList()); } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy index 7cf572ddb1..40ec12da87 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy @@ -20,8 +20,14 @@ package org.onap.cps.ncmp.api.impl +import org.onap.cps.cpspath.parser.PathParsingException import org.onap.cps.ncmp.api.NetworkCmProxyCmHandlerQueryService import org.onap.cps.ncmp.api.inventory.InventoryPersistence +import org.onap.cps.ncmp.api.inventory.CmHandleQueries +import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle +import org.onap.cps.spi.FetchDescendantsOption +import org.onap.cps.spi.exceptions.DataInUseException +import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.model.Anchor import org.onap.cps.spi.model.CmHandleQueryServiceParameters import org.onap.cps.spi.model.ConditionProperties @@ -32,87 +38,127 @@ import java.util.stream.Collectors class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification { + def cmHandleQueries = Mock(CmHandleQueries) def inventoryPersistence = Mock(InventoryPersistence) - NetworkCmProxyCmHandlerQueryService objectUnderTest = new NetworkCmProxyCmHandlerQueryServiceImpl(inventoryPersistence) + def static someCmHandleDataNode = new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'some-cmhandle-id\']', leaves: ['id':'some-cmhandle-id']) + def dmiRegistry = new DataNode(xpath: '/dmi-registry', childDataNodes: createDataNodeList(['PNFDemo1', 'PNFDemo2', 'PNFDemo3', 'PNFDemo4'])) - def 'Retrieve cm handles with public properties when #scenario.'() { - given: 'a condition property' + def objectUnderTest = new NetworkCmProxyCmHandlerQueryServiceImpl(cmHandleQueries, inventoryPersistence) + + def 'Retrieve cm handles with cpsPath when combined with no Module Query.'() { + given: 'a cmHandleWithCpsPath condition property' def cmHandleQueryParameters = new CmHandleQueryServiceParameters() - def conditionProperties = new ConditionProperties() - conditionProperties.conditionName = 'hasAllProperties' - conditionProperties.conditionParameters = publicProperties + def conditionProperties = createConditionProperties('cmHandleWithCpsPath', [['cpsPath' : '/some/cps/path']]) cmHandleQueryParameters.setCmHandleQueryParameters([conditionProperties]) - and: 'mock services' - mockResponses() - when: 'a query is execute (with and without Data)' + and: 'cmHandleQueries returns a non null query result' + cmHandleQueries.getCmHandleDataNodesByCpsPath('/some/cps/path', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [new DataNode(leaves: ['id':'some-cmhandle-id'])] + and: 'CmHandleQueries returns cmHandles with the relevant query result' + cmHandleQueries.combineCmHandleQueries(*_) >> ['PNFDemo1': new NcmpServiceCmHandle(cmHandleId: 'PNFDemo1'), 'PNFDemo3': new NcmpServiceCmHandle(cmHandleId: 'PNFDemo3')] + when: 'the query is executed for both cm handle ids and details' def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters) def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters) then: 'the correct expected cm handles ids are returned' - returnedCmHandlesJustIds == expectedCmHandleIds as Set - and: 'the correct cm handle data objects are returned' - returnedCmHandlesWithData.stream().map(dataNode -> dataNode.cmHandleId).collect(Collectors.toSet()) == expectedCmHandleIds as Set + returnedCmHandlesJustIds == ['PNFDemo1', 'PNFDemo3'] as Set + and: 'the correct ncmp service cm handles are returned' + returnedCmHandlesWithData.stream().map(CmHandle -> CmHandle.cmHandleId).collect(Collectors.toSet()) == ['PNFDemo1', 'PNFDemo3'] as Set + } + + def 'Retrieve cm handles with cpsPath where #scenario.'() { + given: 'a cmHandleWithCpsPath condition property' + def cmHandleQueryParameters = new CmHandleQueryServiceParameters() + def conditionProperties = createConditionProperties('cmHandleWithCpsPath', [['cpsPath' : '/some/cps/path']]) + cmHandleQueryParameters.setCmHandleQueryParameters([conditionProperties]) + and: 'cmHandleQueries throws a path parsing exception' + cmHandleQueries.getCmHandleDataNodesByCpsPath('/some/cps/path', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> { throw thrownException } + when: 'the query is executed for both cm handle ids and details' + objectUnderTest.queryCmHandleIds(cmHandleQueryParameters) + objectUnderTest.queryCmHandles(cmHandleQueryParameters) + then: 'a data validation exception is thrown' + thrown(expectedException) where: 'the following data is used' - scenario | publicProperties || expectedCmHandleIds - 'single property matches' | [['Contact' : 'newemailforstore@bookstore.com']] || ['PNFDemo1', 'PNFDemo2', 'PNFDemo4'] - 'public property does not match' | [['wont_match' : 'wont_match']] || [] - '2 properties, only one match' | [['Contact' : 'newemailforstore@bookstore.com'], ['Contact2': 'newemailforstore2@bookstore.com']] || ['PNFDemo4'] - '2 properties, no matches' | [['Contact' : 'newemailforstore@bookstore.com'], ['Contact2': '']] || [] + scenario | thrownException || expectedException + 'a PathParsingException is thrown' | new PathParsingException('some message', 'some details') || DataValidationException + 'any other Exception is thrown' | new DataInUseException('some message', 'some details') || DataInUseException } - def 'Retrieve cm handles with module names when #scenario.'() { - given: 'a condition property' + def 'Query cm handles with public properties when combined with empty modules query result.'() { + given: 'a public properties condition property' def cmHandleQueryParameters = new CmHandleQueryServiceParameters() - def conditionProperties = new ConditionProperties() - conditionProperties.conditionName = 'hasAllModules' - conditionProperties.conditionParameters = moduleNames + def conditionProperties = createConditionProperties('hasAllProperties', [['some-property-key': 'some-property-value']]) cmHandleQueryParameters.setCmHandleQueryParameters([conditionProperties]) - and: 'mock services' - mockResponses() - when: 'the service is invoked' + and: 'CmHandleQueries returns cmHandles with the relevant query result' + cmHandleQueries.combineCmHandleQueries(*_) >> ['PNFDemo1': new NcmpServiceCmHandle(cmHandleId: 'PNFDemo1'), 'PNFDemo3': new NcmpServiceCmHandle(cmHandleId: 'PNFDemo3')] + when: 'the query is executed for both cm handle ids and details' def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters) def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters) - then: 'the correct expected cm handles are returned' + then: 'the correct expected cm handles ids are returned' + returnedCmHandlesJustIds == ['PNFDemo1', 'PNFDemo3'] as Set + and: 'the correct cm handle data objects are returned' + returnedCmHandlesWithData.stream().map(dataNode -> dataNode.cmHandleId).collect(Collectors.toSet()) == ['PNFDemo1', 'PNFDemo3'] as Set + } + + def 'Retrieve cm handles with module names when #scenario from query.'() { + given: 'a modules condition property' + def cmHandleQueryParameters = new CmHandleQueryServiceParameters() + def conditionProperties = createConditionProperties('hasAllModules', [['moduleName': 'some-module-name']]) + cmHandleQueryParameters.setCmHandleQueryParameters([conditionProperties]) + and: 'null is returned from the state and public property queries' + cmHandleQueries.combineCmHandleQueries(*_) >> null + and: '#scenario from the modules query' + inventoryPersistence.queryAnchors(*_) >> returnedAnchors + and: 'the same cmHandles are returned from the persistence service layer' + returnedAnchors.size() * inventoryPersistence.getDataNode(*_) >> returnedCmHandles + when: 'the query is executed for both cm handle ids and details' + def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters) + def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters) + then: 'the correct expected cm handles ids are returned' returnedCmHandlesJustIds == expectedCmHandleIds as Set + and: 'the correct cm handle data objects are returned' returnedCmHandlesWithData.stream().map(dataNode -> dataNode.cmHandleId).collect(Collectors.toSet()) == expectedCmHandleIds as Set where: 'the following data is used' - scenario | moduleNames || expectedCmHandleIds - 'single matching module name' | [['moduleName' : 'MODULE-NAME-001']] || ['PNFDemo3', 'PNFDemo1', 'PNFDemo2'] - 'module name dont match' | [['moduleName' : 'MODULE-NAME-004']] || [] - '2 module names, only one match' | [['moduleName' : 'MODULE-NAME-002'], ['moduleName': 'MODULE-NAME-003']] || ['PNFDemo4'] - '2 module names, no matches' | [['moduleName' : 'MODULE-NAME-002'], ['moduleName': 'MODULE-NAME-004']] || [] + scenario | returnedAnchors | returnedCmHandles || expectedCmHandleIds + 'One anchor returned' | [new Anchor(name: 'some-cmhandle-id')] | someCmHandleDataNode || ['some-cmhandle-id'] + 'No anchors are returned' | [] | null || [] } def 'Retrieve cm handles with combined queries when #scenario.'() { - given: 'condition properties' + given: 'all condition properties used' def cmHandleQueryParameters = new CmHandleQueryServiceParameters() - def conditionProperties1 = new ConditionProperties() - conditionProperties1.conditionName = 'hasAllProperties' - conditionProperties1.conditionParameters = publicProperties - def conditionProperties2 = new ConditionProperties() - conditionProperties2.conditionName = 'hasAllModules' - conditionProperties2.conditionParameters = moduleNames - cmHandleQueryParameters.setCmHandleQueryParameters([conditionProperties1,conditionProperties2]) - and: 'mock services' - mockResponses() - when: 'the service is invoked' + def conditionPubProps = createConditionProperties('hasAllProperties', [['some-property-key': 'some-property-value']]) + def conditionModules = createConditionProperties('hasAllModules', [['moduleName': 'some-module-name']]) + def conditionState = createConditionProperties('cmHandleWithCpsPath', [['cpsPath' : '/some/cps/path']]) + cmHandleQueryParameters.setCmHandleQueryParameters([conditionPubProps, conditionModules, conditionState]) + and: 'cmHandles are returned from the state and public property combined queries' + cmHandleQueries.combineCmHandleQueries(*_) >> combinedQueryMap + and: 'cmHandles are returned from the module names query' + inventoryPersistence.queryAnchors(['some-module-name']) >> anchorsForModuleQuery + and: 'cmHandleQueries returns a datanode result' + 2 * cmHandleQueries.getCmHandleDataNodesByCpsPath(*_) >> [someCmHandleDataNode] + when: 'the query is executed for both cm handle ids and details' def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters) def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters) - then: 'the correct expected cm handles are returned' + then: 'the correct expected cm handles ids are returned' returnedCmHandlesJustIds == expectedCmHandleIds as Set - returnedCmHandlesWithData.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == expectedCmHandleIds as Set + and: 'the correct cm handle data objects are returned' + returnedCmHandlesWithData.stream().map(dataNode -> dataNode.cmHandleId).collect(Collectors.toSet()) == expectedCmHandleIds as Set where: 'the following data is used' - scenario | moduleNames | publicProperties || expectedCmHandleIds - 'particularly intersect' | [['moduleName' : 'MODULE-NAME-001']] | [['Contact' : 'newemailforstore@bookstore.com']] || ['PNFDemo1', 'PNFDemo2'] - 'empty intersect' | [['moduleName' : 'MODULE-NAME-004']] | [['Contact' : 'newemailforstore@bookstore.com']] || [] - 'total intersect' | [['moduleName' : 'MODULE-NAME-002']] | [['Contact2' : 'newemailforstore2@bookstore.com']] || ['PNFDemo4'] + scenario | combinedQueryMap | anchorsForModuleQuery || expectedCmHandleIds + 'combined and modules queries intersect' | ['PNFDemo1' : new NcmpServiceCmHandle(cmHandleId:'PNFDemo1')] | [new Anchor(name: 'PNFDemo1'), new Anchor(name: 'PNFDemo2')] || ['PNFDemo1'] + 'only module query results exist' | [:] | [new Anchor(name: 'PNFDemo1'), new Anchor(name: 'PNFDemo2')] || [] + 'only combined query results exist' | ['PNFDemo1' : new NcmpServiceCmHandle(cmHandleId:'PNFDemo1'), 'PNFDemo2' : new NcmpServiceCmHandle(cmHandleId:'PNFDemo2')] | [] || [] + 'neither queries return results' | [:] | [] || [] + 'none intersect' | ['PNFDemo1' : new NcmpServiceCmHandle(cmHandleId:'PNFDemo1')] | [new Anchor(name: 'PNFDemo2')] || [] } def 'Retrieve cm handles when the query is empty.'() { - given: 'mock services' - mockResponses() - when: 'the service is invoked' + given: 'We use an empty query' def cmHandleQueryParameters = new CmHandleQueryServiceParameters() + and: 'the inventory persistence returns the dmi registry datanode' + inventoryPersistence.getDataNode("/dmi-registry") >> dmiRegistry + and: 'the inventory persistence returns anchors for get anchors' + inventoryPersistence.getAnchors() >> [new Anchor(name: 'PNFDemo1'), new Anchor(name: 'PNFDemo2'), new Anchor(name: 'PNFDemo3'), new Anchor(name: 'PNFDemo4')] + when: 'the query is executed for both cm handle ids and details' def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters) def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters) then: 'the correct expected cm handles are returned' @@ -120,43 +166,13 @@ class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification { returnedCmHandlesWithData.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == ['PNFDemo1', 'PNFDemo2', 'PNFDemo3', 'PNFDemo4'] as Set } - void mockResponses() { - def pNFDemo1 = new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'PNFDemo1\']', leaves: ['id':'PNFDemo1']) - def pNFDemo2 = new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'PNFDemo2\']', leaves: ['id':'PNFDemo2']) - def pNFDemo3 = new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'PNFDemo3\']', leaves: ['id':'PNFDemo3']) - def pNFDemo4 = new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'PNFDemo4\']', leaves: ['id':'PNFDemo4']) - def dmiRegistry = new DataNode(xpath: '/dmi-registry', childDataNodes: [pNFDemo1, pNFDemo2, pNFDemo3, pNFDemo4]) - - inventoryPersistence.queryDataNodes('//public-properties[@name=\'Contact\' and @value=\'newemailforstore@bookstore.com\']/ancestor::cm-handles') - >> [pNFDemo1, pNFDemo2, pNFDemo4] - inventoryPersistence.queryDataNodes('//public-properties[@name=\'wont_match\' and @value=\'wont_match\']/ancestor::cm-handles') - >> [] - inventoryPersistence.queryDataNodes('//public-properties[@name=\'Contact2\' and @value=\'newemailforstore2@bookstore.com\']/ancestor::cm-handles') - >> [pNFDemo4] - inventoryPersistence.queryDataNodes('//public-properties[@name=\'Contact2\' and @value=\'\']/ancestor::cm-handles') - >> [] - inventoryPersistence.queryDataNodes('//public-properties/ancestor::cm-handles') - >> [pNFDemo1, pNFDemo2, pNFDemo3, pNFDemo4] - - inventoryPersistence.queryDataNodes('//cm-handles[@id=\'PNFDemo\']') >> [pNFDemo1] - inventoryPersistence.queryDataNodes('//cm-handles[@id=\'PNFDemo2\']') >> [pNFDemo2] - inventoryPersistence.queryDataNodes('//cm-handles[@id=\'PNFDemo3\']') >> [pNFDemo3] - inventoryPersistence.queryDataNodes('//cm-handles[@id=\'PNFDemo4\']') >> [pNFDemo4] - - inventoryPersistence.getDataNode('/dmi-registry') >> dmiRegistry - - inventoryPersistence.getDataNode('/dmi-registry/cm-handles[@id=\'PNFDemo1\']') >> pNFDemo1 - inventoryPersistence.getDataNode('/dmi-registry/cm-handles[@id=\'PNFDemo2\']') >> pNFDemo2 - inventoryPersistence.getDataNode('/dmi-registry/cm-handles[@id=\'PNFDemo3\']') >> pNFDemo3 - inventoryPersistence.getDataNode('/dmi-registry/cm-handles[@id=\'PNFDemo4\']') >> pNFDemo4 + def createConditionProperties(String conditionName, List<Map<String, String>> conditionParameters) { + return new ConditionProperties(conditionName : conditionName, conditionParameters : conditionParameters) + } - inventoryPersistence.queryAnchors(['MODULE-NAME-001']) >> [new Anchor(name: 'PNFDemo1'), new Anchor(name: 'PNFDemo2'), new Anchor(name: 'PNFDemo3')] - inventoryPersistence.queryAnchors(['MODULE-NAME-004']) >> [] - inventoryPersistence.queryAnchors(['MODULE-NAME-003', 'MODULE-NAME-002']) >> [new Anchor(name: 'PNFDemo4')] - inventoryPersistence.queryAnchors(['MODULE-NAME-002', 'MODULE-NAME-003']) >> [new Anchor(name: 'PNFDemo4')] - inventoryPersistence.queryAnchors(['MODULE-NAME-004', 'MODULE-NAME-002']) >> [] - inventoryPersistence.queryAnchors(['MODULE-NAME-002', 'MODULE-NAME-004']) >> [] - inventoryPersistence.queryAnchors(['MODULE-NAME-002']) >> [new Anchor(name: 'PNFDemo2'), new Anchor(name: 'PNFDemo4')] - inventoryPersistence.getAnchors() >> [new Anchor(name: 'PNFDemo1'), new Anchor(name: 'PNFDemo2'), new Anchor(name: 'PNFDemo3'), new Anchor(name: 'PNFDemo4')] + def static createDataNodeList(dataNodeIds) { + def dataNodes =[] + dataNodeIds.forEach(id -> {dataNodes.add(new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'' + id + '\']', leaves: ['id':id]))}) + return dataNodes } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesSpec.groovy new file mode 100644 index 0000000000..10a5d62461 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesSpec.groovy @@ -0,0 +1,150 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 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.inventory + +import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle +import org.onap.cps.spi.CpsDataPersistenceService +import org.onap.cps.spi.model.DataNode +import spock.lang.Shared +import spock.lang.Specification + +import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS +import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS + +class CmHandleQueriesSpec extends Specification { + def cpsDataPersistenceService = Mock(CpsDataPersistenceService) + + def objectUnderTest = new CmHandleQueries(cpsDataPersistenceService) + + @Shared + def static sampleDataNodes = [new DataNode()] + + def static pnfDemo = createDataNode('PNFDemo') + def static pnfDemo2 = createDataNode('PNFDemo2') + def static pnfDemo3 = createDataNode('PNFDemo3') + def static pnfDemo4 = createDataNode('PNFDemo4') + + def static pnfDemoCmHandle = new NcmpServiceCmHandle(cmHandleId: 'PNFDemo') + def static pnfDemo2CmHandle = new NcmpServiceCmHandle(cmHandleId: 'PNFDemo2') + def static pnfDemo3CmHandle = new NcmpServiceCmHandle(cmHandleId: 'PNFDemo3') + + def 'Query CmHandles with public properties query pair.'() { + given: 'the DataNodes queried for a given cpsPath are returned from the persistence service.' + mockResponses() + when: 'a query on cmhandle public properties is performed with a public property pair' + def returnedCmHandlesWithData = objectUnderTest.queryCmHandlePublicProperties(publicPropertyPairs) + then: 'the correct cm handle data objects are returned' + returnedCmHandlesWithData.keySet().containsAll(expectedCmHandleIds) + returnedCmHandlesWithData.keySet().size() == expectedCmHandleIds.size() + where: 'the following data is used' + scenario | publicPropertyPairs || expectedCmHandleIds + 'single property matches' | ['Contact' : 'newemailforstore@bookstore.com'] || ['PNFDemo', 'PNFDemo2', 'PNFDemo4'] + 'public property does not match' | ['wont_match' : 'wont_match'] || [] + '2 properties, only one match' | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': 'newemailforstore2@bookstore.com'] || ['PNFDemo4'] + '2 properties, no matches' | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': ''] || [] + } + + def 'Query CmHandles using empty public properties query pair.'() { + when: 'a query on CmHandle public properties is executed using an empty map' + def returnedCmHandlesWithData = objectUnderTest.queryCmHandlePublicProperties([:]) + then: 'no cm handles are returned' + returnedCmHandlesWithData.keySet().size() == 0 + } + + def 'Combine two query results where #scenario.'() { + when: 'two query results in the form of a map of NcmpServiceCmHandles are combined into a single query result' + def result = objectUnderTest.combineCmHandleQueries(firstQuery, secondQuery) + then: 'the returned result is the same as the expected result' + result == expectedResult + where: + scenario | firstQuery | secondQuery || expectedResult + 'two queries with unique and non unique entries exist' | ['PNFDemo': pnfDemoCmHandle, 'PNFDemo2': pnfDemo2CmHandle] | ['PNFDemo': pnfDemoCmHandle, 'PNFDemo3': pnfDemo3CmHandle] || ['PNFDemo': pnfDemoCmHandle] + 'the first query contains entries and second query is empty' | ['PNFDemo': pnfDemoCmHandle, 'PNFDemo2': pnfDemo2CmHandle] | [:] || [:] + 'the second query contains entries and first query is empty' | [:] | ['PNFDemo': pnfDemoCmHandle, 'PNFDemo3': pnfDemo3CmHandle] || [:] + 'the first query contains entries and second query is null' | ['PNFDemo': pnfDemoCmHandle, 'PNFDemo2': pnfDemo2CmHandle] | null || ['PNFDemo': pnfDemoCmHandle, 'PNFDemo2': pnfDemo2CmHandle] + 'the second query contains entries and first query is null' | null | ['PNFDemo': pnfDemoCmHandle, 'PNFDemo3': pnfDemo3CmHandle] || ['PNFDemo': pnfDemoCmHandle, 'PNFDemo3': pnfDemo3CmHandle] + 'both queries are empty' | [:] | [:] || [:] + 'both queries are null' | null | null || [:] + } + + def 'Get Cm Handles By State'() { + given: 'a cm handle state to query' + def cmHandleState = CmHandleState.ADVISED + and: 'the persistence service returns a list of data nodes' + cpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry', + '//state[@cm-handle-state="ADVISED"]/ancestor::cm-handles', INCLUDE_ALL_DESCENDANTS) >> sampleDataNodes + when: 'cm handles are fetched by state' + def result = objectUnderTest.getCmHandlesByState(cmHandleState) + then: 'the returned result matches the result from the persistence service' + assert result == sampleDataNodes + } + + def 'Get Cm Handles By State and Cm-Handle Id'() { + given: 'a cm handle state to query' + def cmHandleState = CmHandleState.READY + and: 'cps data service returns a list of data nodes' + cpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry', + '//cm-handles[@id=\'some-cm-handle\']/state[@cm-handle-state="'+ 'READY'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes + when: 'cm handles are fetched by state and id' + def result = objectUnderTest.getCmHandlesByIdAndState('some-cm-handle', cmHandleState) + then: 'the returned result is a list of data nodes returned by cps data service' + assert result == sampleDataNodes + } + + def 'Get Cm Handles By Operational Sync State : UNSYNCHRONIZED'() { + given: 'a cm handle state to query' + def cmHandleState = CmHandleState.READY + and: 'cps data service returns a list of data nodes' + cpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry', + '//state/datastores/operational[@sync-state="'+'UNSYNCHRONIZED'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes + when: 'cm handles are fetched by the UNSYNCHRONIZED operational sync state' + def result = objectUnderTest.getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) + then: 'the returned result is a list of data nodes returned by cps data service' + assert result == sampleDataNodes + } + + def 'Retrieve cm handle by cps path '() { + given: 'a cm handle state to query based on the cps path' + def cmHandleDataNode = new DataNode(xpath: 'xpath', leaves: ['cm-handle-state': 'LOCKED']) + def cpsPath = '//cps-path' + and: 'cps data service returns a valid data node' + cpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry', + cpsPath + '/ancestor::cm-handles', INCLUDE_ALL_DESCENDANTS) + >> Arrays.asList(cmHandleDataNode) + when: 'get cm handles by cps path is invoked' + def result = objectUnderTest.getCmHandleDataNodesByCpsPath(cpsPath, INCLUDE_ALL_DESCENDANTS) + then: 'the returned result is a list of data nodes returned by cps data service' + assert result.contains(cmHandleDataNode) + } + + void mockResponses() { + cpsDataPersistenceService.queryDataNodes(_, _, '//public-properties[@name=\"Contact\" and @value=\"newemailforstore@bookstore.com\"]/ancestor::cm-handles', _) >> [pnfDemo, pnfDemo2, pnfDemo4] + cpsDataPersistenceService.queryDataNodes(_, _, '//public-properties[@name=\"wont_match\" and @value=\"wont_match\"]/ancestor::cm-handles', _) >> [] + cpsDataPersistenceService.queryDataNodes(_, _, '//public-properties[@name=\"Contact2\" and @value=\"newemailforstore2@bookstore.com\"]/ancestor::cm-handles', _) >> [pnfDemo4] + cpsDataPersistenceService.queryDataNodes(_, _, '//public-properties[@name=\"Contact2\" and @value=\"\"]/ancestor::cm-handles', _) >> [] + cpsDataPersistenceService.queryDataNodes(_, _, '//state[@cm-handle-state=\"READY\"]/ancestor::cm-handles', _) >> [pnfDemo, pnfDemo3] + cpsDataPersistenceService.queryDataNodes(_, _, '//state[@cm-handle-state=\"LOCKED\"]/ancestor::cm-handles', _) >> [pnfDemo2, pnfDemo4] + } + + def static createDataNode(dataNodeId) { + return new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'' + dataNodeId + '\']', leaves: ['id':dataNodeId]) + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceSpec.groovy index 7ac231c169..f9ca676f3b 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceSpec.groovy @@ -80,9 +80,6 @@ class InventoryPersistenceSpec extends Specification { @Shared def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])] - @Shared - def static sampleDataNodes = [new DataNode()] - def "Retrieve CmHandle using datanode with #scenario."() { given: 'the cps data service returns a data node from the DMI registry' def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves) @@ -157,56 +154,6 @@ class InventoryPersistenceSpec extends Specification { 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}' } - def 'Get Cm Handles By State'() { - given: 'a cm handle state to query' - def cmHandleState = CmHandleState.ADVISED - and: 'cps data service returns a list of data nodes' - mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry', - '//state[@cm-handle-state="ADVISED"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes - when: 'get cm handles by state is invoked' - def result = objectUnderTest.getCmHandlesByState(cmHandleState) - then: 'the returned result is a list of data nodes returned by cps data service' - assert result == sampleDataNodes - } - - def 'Get Cm Handles By State and Cm-Handle Id'() { - given: 'a cm handle state to query' - def cmHandleState = CmHandleState.READY - and: 'cps data service returns a list of data nodes' - mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry', - '//cm-handles[@id=\'some-cm-handle\']/state[@cm-handle-state="'+ 'READY'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes - when: 'get cm handles by state and id is invoked' - def result = objectUnderTest.getCmHandlesByIdAndState(cmHandleId, cmHandleState) - then: 'the returned result is a list of data nodes returned by cps data service' - assert result == sampleDataNodes - } - - def 'Get Cm Handles By Operational Sync State : UNSYNCHRONIZED'() { - given: 'a cm handle state to query' - def cmHandleState = CmHandleState.READY - and: 'cps data service returns a list of data nodes' - mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry', - '//state/datastores/operational[@sync-state="'+'UNSYNCHRONIZED'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes - when: 'get cm handles by operational sync state as UNSYNCHRONIZED is invoked' - def result = objectUnderTest.getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) - then: 'the returned result is a list of data nodes returned by cps data service' - assert result == sampleDataNodes - } - - def 'Retrieve cm handle by cps path '() { - given: 'a cm handle state to query based on the cps path' - def cmHandleDataNode = new DataNode(xpath: 'xpath', leaves: ['cm-handle-state': 'LOCKED']) - def cpsPath = '//cps-path' - and: 'cps data service returns a valid data node' - mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry', - cpsPath, INCLUDE_ALL_DESCENDANTS) - >> Arrays.asList(cmHandleDataNode) - when: 'get cm handles by cps path is invoked' - def result = objectUnderTest.getCmHandleDataNodesByCpsPath(cpsPath, INCLUDE_ALL_DESCENDANTS) - then: 'the returned result is a list of data nodes returned by cps data service' - assert result.contains(cmHandleDataNode) - } - def 'Get module definitions'() { given: 'cps module service returns a collection of module definitions' def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')] @@ -263,13 +210,6 @@ class InventoryPersistenceSpec extends Specification { 0 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'sampleSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED) } - def 'Query data nodes via cpsPath'() { - when: 'the method to query data nodes is called' - objectUnderTest.queryDataNodes('sample cpsPath') - then: 'the data persistence service method to query data nodes is invoked once' - 1 * mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin','ncmp-dmi-registry','sample cpsPath', INCLUDE_ALL_DESCENDANTS) - } - def 'Get data node via xPath'() { when: 'the method to get data nodes is called' objectUnderTest.getDataNode('sample xPath') diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/SyncUtilsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/SyncUtilsSpec.groovy index 6c2d8f15b3..82e9d33ae7 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/SyncUtilsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/SyncUtilsSpec.groovy @@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations import org.onap.cps.ncmp.api.impl.operations.DmiOperations +import org.onap.cps.ncmp.api.inventory.CmHandleQueries import org.onap.cps.ncmp.api.inventory.CmHandleState import org.onap.cps.ncmp.api.inventory.CompositeState import org.onap.cps.ncmp.api.inventory.CompositeStateBuilder @@ -47,11 +48,13 @@ class SyncUtilsSpec extends Specification{ def mockInventoryPersistence = Mock(InventoryPersistence) + def mockCmHandleQueries = Mock(CmHandleQueries) + def mockDmiDataOperations = Mock(DmiDataOperations) def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) - def objectUnderTest = new SyncUtils(mockInventoryPersistence, mockDmiDataOperations, jsonObjectMapper) + def objectUnderTest = new SyncUtils(mockInventoryPersistence, mockCmHandleQueries, mockDmiDataOperations, jsonObjectMapper) @Shared def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(OffsetDateTime.now()) @@ -61,14 +64,14 @@ class SyncUtilsSpec extends Specification{ def 'Get an advised Cm-Handle where ADVISED cm handle #scenario'() { given: 'the inventory persistence service returns a collection of data nodes' - mockInventoryPersistence.getCmHandlesByState(CmHandleState.ADVISED) >> dataNodeCollection + mockCmHandleQueries.getCmHandlesByState(CmHandleState.ADVISED) >> dataNodeCollection when: 'get advised cm handles are fetched' def yangModelCmHandles = objectUnderTest.getAdvisedCmHandles() then: 'the returned data node collection is the correct size' yangModelCmHandles.size() == expectedDataNodeSize and: 'yang model collection contains the correct data' yangModelCmHandles.stream().map(yangModel -> yangModel.id).collect(Collectors.toSet()) == - dataNodeCollection.stream().map(dataNode -> dataNode.leaves.get("id")).collect(Collectors.toSet()) + dataNodeCollection.stream().map(dataNode -> dataNode.leaves.get("id")).collect(Collectors.toSet()) where: 'the following scenarios are used' scenario | dataNodeCollection || expectedCallsToGetYangModelCmHandle | expectedDataNodeSize 'exists' | [dataNode] || 1 | 1 @@ -77,7 +80,7 @@ class SyncUtilsSpec extends Specification{ def 'Update Lock Reason, Details and Attempts where lock reason #scenario'() { given: 'A locked state' - def compositeState = new CompositeState(lockReason: lockReason) + def compositeState = new CompositeState(lockReason: lockReason) when: 'update cm handle details and attempts is called' objectUnderTest.updateLockReasonDetailsAndAttempts(compositeState, LockReasonCategory.LOCKED_MODULE_SYNC_FAILED, 'new error message') then: 'the composite state lock reason and details are updated' @@ -91,9 +94,9 @@ class SyncUtilsSpec extends Specification{ def 'Get all locked Cm-Handle where Lock Reason is LOCKED_MODULE_SYNC_FAILED cm handle #scenario'() { given: 'the cps (persistence service) returns a collection of data nodes' - mockInventoryPersistence.getCmHandleDataNodesByCpsPath( - '//lock-reason[@reason="LOCKED_MODULE_SYNC_FAILED"]/ancestor::cm-handles', - FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode ] + mockCmHandleQueries.getCmHandleDataNodesByCpsPath( + '//lock-reason[@reason="LOCKED_MODULE_SYNC_FAILED"]', + FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode] when: 'get locked Misbehaving cm handle is called' def result = objectUnderTest.getModuleSyncFailedCmHandles() then: 'the returned cm handle collection is the correct size' @@ -119,8 +122,8 @@ class SyncUtilsSpec extends Specification{ def 'Get a Cm-Handle where Operational Sync state is UnSynchronized and Cm-handle state is READY and #scenario'() { given: 'the inventory persistence service returns a collection of data nodes' - mockInventoryPersistence.getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) >> unSynchronizedDataNodes - mockInventoryPersistence.getCmHandlesByIdAndState("cm-handle-123", CmHandleState.READY) >> readyDataNodes + mockCmHandleQueries.getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) >> unSynchronizedDataNodes + mockCmHandleQueries.getCmHandlesByIdAndState("cm-handle-123", CmHandleState.READY) >> readyDataNodes when: 'get advised cm handles are fetched' objectUnderTest.getAnUnSynchronizedReadyCmHandle() then: 'the returned data node collection is the correct size' |