From 4266fbbaad4ab3741accf1b3911278835ca19795 Mon Sep 17 00:00:00 2001 From: sourabh_sourabh Date: Mon, 23 Oct 2023 14:40:25 +0100 Subject: Introduce and use new Hazelcast map pt. 2 - modified ModuleSyncService - created ModuleSetTagCacheConfig and tests - ensure both Create and Upgrade cm Handle use cases work - Moved inventory pkg to its right patha as production code. Issue-ID: CPS-1859 Signed-off-by: leventecsanyi Change-Id: I173dcd81fe2e74d86d85365b2ead461302cad974 Signed-off-by: sourabh_sourabh --- .../ModuleSetTagCacheConfigSpec.groovy | 76 +++++ .../SynchronizationCacheConfigSpec.groovy | 32 +-- .../impl/inventory/CmHandleQueriesImplSpec.groovy | 201 +++++++++++++ .../inventory/CompositeStateBuilderSpec.groovy | 92 ++++++ .../api/impl/inventory/CompositeStateSpec.groovy | 64 +++++ .../inventory/InventoryPersistenceImplSpec.groovy | 311 +++++++++++++++++++++ .../inventory/sync/DataSyncWatchdogSpec.groovy | 118 ++++++++ .../inventory/sync/ModuleSyncServiceSpec.groovy | 187 +++++++++++++ .../impl/inventory/sync/ModuleSyncTasksSpec.groovy | 217 ++++++++++++++ .../inventory/sync/ModuleSyncWatchdogSpec.groovy | 124 ++++++++ .../api/impl/inventory/sync/SyncUtilsSpec.groovy | 198 +++++++++++++ .../config/WatchdogSchedulingConfigurerSpec.groovy | 59 ++++ .../sync/executor/AsyncTaskExecutorSpec.groovy | 62 ++++ .../api/inventory/CmHandleQueriesImplSpec.groovy | 201 ------------- .../api/inventory/CompositeStateBuilderSpec.groovy | 92 ------ .../ncmp/api/inventory/CompositeStateSpec.groovy | 64 ----- .../inventory/InventoryPersistenceImplSpec.groovy | 311 --------------------- .../api/inventory/sync/DataSyncWatchdogSpec.groovy | 118 -------- .../inventory/sync/ModuleSyncServiceSpec.groovy | 155 ---------- .../api/inventory/sync/ModuleSyncTasksSpec.groovy | 217 -------------- .../inventory/sync/ModuleSyncWatchdogSpec.groovy | 126 --------- .../ncmp/api/inventory/sync/SyncUtilsSpec.groovy | 198 ------------- .../config/WatchdogSchedulingConfigurerSpec.groovy | 59 ---- .../sync/executor/AsyncTaskExecutorSpec.groovy | 62 ---- 24 files changed, 1712 insertions(+), 1632 deletions(-) create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/embeddedcache/ModuleSetTagCacheConfigSpec.groovy create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/CmHandleQueriesImplSpec.groovy create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/CompositeStateBuilderSpec.groovy create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/CompositeStateSpec.groovy create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImplSpec.groovy create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/DataSyncWatchdogSpec.groovy create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncServiceSpec.groovy create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncTasksSpec.groovy create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncWatchdogSpec.groovy create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/SyncUtilsSpec.groovy create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/config/WatchdogSchedulingConfigurerSpec.groovy create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/executor/AsyncTaskExecutorSpec.groovy delete mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImplSpec.groovy delete mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CompositeStateBuilderSpec.groovy delete mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CompositeStateSpec.groovy delete mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy delete mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/DataSyncWatchdogSpec.groovy delete mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncServiceSpec.groovy delete mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncTasksSpec.groovy delete mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncWatchdogSpec.groovy delete mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/SyncUtilsSpec.groovy delete mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/config/WatchdogSchedulingConfigurerSpec.groovy delete mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/executor/AsyncTaskExecutorSpec.groovy (limited to 'cps-ncmp-service/src/test/groovy/org/onap') diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/embeddedcache/ModuleSetTagCacheConfigSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/embeddedcache/ModuleSetTagCacheConfigSpec.groovy new file mode 100644 index 0000000000..8a6a32bd8d --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/embeddedcache/ModuleSetTagCacheConfigSpec.groovy @@ -0,0 +1,76 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2023 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.impl.config.embeddedcache + +import com.hazelcast.config.Config +import com.hazelcast.core.Hazelcast +import org.onap.cps.spi.model.ModuleReference +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import spock.lang.Specification + +@SpringBootTest(classes = [ModuleSetTagCacheConfig]) +class ModuleSetTagCacheConfigSpec extends Specification { + + @Autowired + private Map> moduleSetTagCache + + def 'Embedded (hazelcast) caches for module set tag.'() { + expect: 'system is able to create an instance of a map to hold module set tags' + assert null != moduleSetTagCache + and: 'there is at least 1 instance' + assert Hazelcast.allHazelcastInstances.size() > 0 + and: 'hazelcast instance name of module set tag is correct' + assert Hazelcast.allHazelcastInstances.name.contains('moduleSetTags') + } + + def 'Verify configs of module set tag distributed object.'(){ + when: 'retrieving the map config of module set tag' + def cacheConfig = Hazelcast.getHazelcastInstanceByName('moduleSetTags').config + def moduleSetTagMapConfig = cacheConfig.mapConfigs.get('moduleSetTagCacheMapConfig') + then: 'the module set tag map config has correct backup counts' + assert moduleSetTagMapConfig.backupCount == 3 + assert moduleSetTagMapConfig.asyncBackupCount == 3 + } + + def 'Verify deployment network configs of distributed cache of module set tag object.'() { + given: 'network config of module set tag cache' + def moduleSetTagNetworkConfig = Hazelcast.getHazelcastInstanceByName('moduleSetTags').config.networkConfig + expect: 'module set tag cache has the correct settings' + assert moduleSetTagNetworkConfig.join.autoDetectionConfig.enabled + assert !moduleSetTagNetworkConfig.join.kubernetesConfig.enabled + } + + def 'Verify network of module set tag cache'() { + given: 'Synchronization config object and test configuration' + def objectUnderTest = new ModuleSetTagCacheConfig() + def testConfig = new Config() + when: 'kubernetes properties are enabled' + objectUnderTest.cacheKubernetesEnabled = true + objectUnderTest.cacheKubernetesServiceName = 'test-service-name' + and: 'method called to update the discovery mode' + objectUnderTest.updateDiscoveryMode(testConfig) + then: 'applied properties are reflected' + assert testConfig.networkConfig.join.kubernetesConfig.enabled + assert testConfig.networkConfig.join.kubernetesConfig.properties.get('service-name') == 'test-service-name' + + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/embeddedcache/SynchronizationCacheConfigSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/embeddedcache/SynchronizationCacheConfigSpec.groovy index 2fa9606923..501714a2be 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/embeddedcache/SynchronizationCacheConfigSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/embeddedcache/SynchronizationCacheConfigSpec.groovy @@ -44,9 +44,6 @@ class SynchronizationCacheConfigSpec extends Specification { @Autowired private IMap dataSyncSemaphores - @Autowired - private IMap> moduleSetTagCache - def 'Embedded (hazelcast) Caches for Module and Data Sync.'() { expect: 'system is able to create an instance of the Module Sync Work Queue' assert null != moduleSyncWorkQueue @@ -54,12 +51,10 @@ class SynchronizationCacheConfigSpec extends Specification { assert null != moduleSyncStartedOnCmHandles and: 'system is able to create an instance of a map to hold data sync semaphores' assert null != dataSyncSemaphores - and: 'system is able to create an instance of a map to hold module set tags' - assert null != moduleSetTagCache - and: 'there are at least 4 instances' - assert Hazelcast.allHazelcastInstances.size() > 3 + and: 'there are at least 3 instances' + assert Hazelcast.allHazelcastInstances.size() > 2 and: 'they have the correct names (in any order)' - assert Hazelcast.allHazelcastInstances.name.containsAll('moduleSyncWorkQueue', 'moduleSyncStartedOnCmHandles', 'dataSyncSemaphores', 'moduleSetTags') + assert Hazelcast.allHazelcastInstances.name.containsAll('moduleSyncWorkQueue', 'moduleSyncStartedOnCmHandles', 'dataSyncSemaphores') } def 'Verify configs for Distributed objects'(){ @@ -72,9 +67,6 @@ class SynchronizationCacheConfigSpec extends Specification { and: 'the Data Sync Semaphores Map config' def dataSyncSemaphoresConfig = Hazelcast.getHazelcastInstanceByName('dataSyncSemaphores').config def dataSyncSemaphoresMapConfig = dataSyncSemaphoresConfig.mapConfigs.get('dataSyncSemaphoresConfig') - and: 'the Module Set Tag Map config' - def moduleSetTagCacheConfig = Hazelcast.getHazelcastInstanceByName('moduleSetTags').config - def moduleSetTagMapConfig = moduleSetTagCacheConfig.mapConfigs.get('moduleSetTagCacheMapConfig') expect: 'system created instance with correct config of Module Sync Work Queue' assert moduleSyncDefaultWorkQueueConfig.backupCount == 3 assert moduleSyncDefaultWorkQueueConfig.asyncBackupCount == 3 @@ -84,15 +76,11 @@ class SynchronizationCacheConfigSpec extends Specification { and: 'Data Sync Semaphore Map has the correct settings' assert dataSyncSemaphoresMapConfig.backupCount == 3 assert dataSyncSemaphoresMapConfig.asyncBackupCount == 3 - and: 'Module Set Tag Map has the correct settings' - assert moduleSetTagMapConfig.backupCount == 3 - assert moduleSetTagMapConfig.asyncBackupCount == 3 and: 'all instances are part of same cluster' def testClusterName = 'cps-and-ncmp-test-caches' assert moduleSyncWorkQueueConfig.clusterName == testClusterName assert moduleSyncStartedOnCmHandlesConfig.clusterName == testClusterName assert dataSyncSemaphoresConfig.clusterName == testClusterName - assert moduleSetTagCacheConfig.clusterName == testClusterName } def 'Verify deployment network configs for Distributed objects'() { @@ -102,8 +90,6 @@ class SynchronizationCacheConfigSpec extends Specification { def moduleSyncStartedOnCmHandlesNetworkConfig = Hazelcast.getHazelcastInstanceByName('moduleSyncStartedOnCmHandles').config.networkConfig and: 'the Data Sync Semaphores Map config' def dataSyncSemaphoresNetworkConfig = Hazelcast.getHazelcastInstanceByName('dataSyncSemaphores').config.networkConfig - and: 'the Module Set Tag Map config' - def moduleSetTagNetworkConfig = Hazelcast.getHazelcastInstanceByName('moduleSetTags').config.networkConfig expect: 'system created instance with correct config of Module Sync Work Queue' assert queueNetworkConfig.join.autoDetectionConfig.enabled assert !queueNetworkConfig.join.kubernetesConfig.enabled @@ -113,9 +99,6 @@ class SynchronizationCacheConfigSpec extends Specification { and: 'Data Sync Semaphore Map has the correct settings' assert dataSyncSemaphoresNetworkConfig.join.autoDetectionConfig.enabled assert !dataSyncSemaphoresNetworkConfig.join.kubernetesConfig.enabled - and: 'Module Set Tag Map has the correct settings' - assert moduleSetTagNetworkConfig.join.autoDetectionConfig.enabled - assert !moduleSetTagNetworkConfig.join.kubernetesConfig.enabled } def 'Verify network config'() { @@ -151,15 +134,6 @@ class SynchronizationCacheConfigSpec extends Specification { waitMax2SecondsForKeyExpiration(dataSyncSemaphores, 'testKeyDataSync') } - def 'Time to Live Verify for Module Set Tag'() { - when: 'the key is inserted with a TTL of 1 second' - moduleSetTagCache.put('testKeyModuleSetTag', ['module-set-tag'] as Set, 1, TimeUnit.SECONDS) - then: 'the entry is present in the map' - assert moduleSetTagCache.get('testKeyModuleSetTag') != null - and: 'the entry expires in less then 2 seconds' - waitMax2SecondsForKeyExpiration(moduleSetTagCache, 'testKeyModuleSetTag') - } - def waitMax2SecondsForKeyExpiration(map, key) { def count = 0 while ( map.get(key)!=null && ++count <= 20 ) { diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/CmHandleQueriesImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/CmHandleQueriesImplSpec.groovy new file mode 100644 index 0000000000..78b09e6a15 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/CmHandleQueriesImplSpec.groovy @@ -0,0 +1,201 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2023 Nordix Foundation + * Modifications Copyright (C) 2023 TechMahindra Ltd. + * ================================================================================ + * 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.impl.inventory + +import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel + +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.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS +import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS + +import com.hazelcast.map.IMap +import org.onap.cps.ncmp.api.impl.inventory.CmHandleQueriesImpl +import org.onap.cps.ncmp.api.impl.inventory.CmHandleState +import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState +import org.onap.cps.spi.CpsDataPersistenceService +import org.onap.cps.spi.model.DataNode +import spock.lang.Shared +import spock.lang.Specification + +class CmHandleQueriesImplSpec extends Specification { + def cpsDataPersistenceService = Mock(CpsDataPersistenceService) + def trustLevelPerCmHandle = [ 'my completed cm handle': TrustLevel.COMPLETE, 'my untrusted cm handle': TrustLevel.NONE ] + + def objectUnderTest = new CmHandleQueriesImpl(cpsDataPersistenceService, trustLevelPerCmHandle) + + @Shared + def static sampleDataNodes = [new DataNode()] + + def dataNodeWithPrivateField = '//additional-properties[@name=\"Contact3\" and @value=\"newemailforstore3@bookstore.com\"]/ancestor::cm-handles' + + def static pnfDemo = createDataNode('PNFDemo') + def static pnfDemo2 = createDataNode('PNFDemo2') + def static pnfDemo3 = createDataNode('PNFDemo3') + def static pnfDemo4 = createDataNode('PNFDemo4') + def static pnfDemo5 = createDataNode('PNFDemo5') + + 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 result = objectUnderTest.queryCmHandlePublicProperties(publicPropertyPairs) + then: 'the correct cm handle data objects are returned' + result.containsAll(expectedCmHandleIds) + result.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 cm handles on trust level'() { + given: 'query properties for trustlevel COMPLETE' + def trustLevelPropertyQueryPairs = ['trustLevel' : TrustLevel.COMPLETE.toString()] + when: 'the query is executed' + def result = objectUnderTest.queryCmHandlesByTrustLevel(trustLevelPropertyQueryPairs) + then: 'the result only contains the completed cm handle' + assert result.size() == 1 + assert result[0] == 'my completed cm handle' + } + + def 'Query CmHandles using empty public properties query pair.'() { + when: 'a query on CmHandle public properties is executed using an empty map' + def result = objectUnderTest.queryCmHandlePublicProperties([:]) + then: 'no cm handles are returned' + result.size() == 0 + } + + def 'Query CmHandles using empty private properties query pair.'() { + when: 'a query on CmHandle private properties is executed using an empty map' + def result = objectUnderTest.queryCmHandleAdditionalProperties([:]) + then: 'no cm handles are returned' + result.size() == 0 + } + + def 'Query CmHandles by a private field\'s value.'() { + given: 'a data node exists with a certain additional-property' + cpsDataPersistenceService.queryDataNodes(_, _, dataNodeWithPrivateField, _) >> [pnfDemo5] + when: 'a query on CmHandle private properties is executed using a map' + def result = objectUnderTest.queryCmHandleAdditionalProperties(['Contact3': 'newemailforstore3@bookstore.com']) + then: 'one cm handle is returned' + result.size() == 1 + } + + def 'Get CmHandles by it\'s 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_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + '//state[@cm-handle-state="ADVISED"]/ancestor::cm-handles', INCLUDE_ALL_DESCENDANTS) >> sampleDataNodes + when: 'cm handles are fetched by state' + def result = objectUnderTest.queryCmHandlesByState(cmHandleState) + then: 'the returned result matches the result from the persistence service' + assert result == sampleDataNodes + } + + def 'Check the state of a cmHandle when #scenario.'() { + given: 'a cm handle state to compare' + def cmHandleState = state + and: 'the persistence service returns a list of data nodes' + cpsDataPersistenceService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + NCMP_DMI_REGISTRY_PARENT + '/cm-handles[@id=\'some-cm-handle\']/state', + OMIT_DESCENDANTS) >> [new DataNode(leaves: ['cm-handle-state': 'READY'])] + when: 'cm handles are compared by state' + def result = objectUnderTest.cmHandleHasState('some-cm-handle', cmHandleState) + then: 'the returned result matches the expected result from the persistence service' + result == expectedResult + where: + scenario | state || expectedResult + 'the provided state matches' | CmHandleState.READY || true + 'the provided state does not match'| CmHandleState.DELETED || false + } + + def 'Get Cm Handles state by 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.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + NCMP_DMI_REGISTRY_PARENT + '/cm-handles[@id=\'some-cm-handle\']/state', + OMIT_DESCENDANTS) >> [new DataNode(leaves: ['cm-handle-state': 'READY'])] + when: 'cm handles are fetched by state and id' + def result = objectUnderTest.getCmHandleState('some-cm-handle') + then: 'the returned result is a list of data nodes returned by cps data service' + assert result == new DataNode(leaves: ['cm-handle-state': 'READY']) + } + + def 'Retrieve 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_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + '//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.queryCmHandlesByOperationalSyncState(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_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + cpsPath + '/ancestor::cm-handles', INCLUDE_ALL_DESCENDANTS) + >> Arrays.asList(cmHandleDataNode) + when: 'get cm handles by cps path is invoked' + def result = objectUnderTest.queryCmHandleAncestorsByCpsPath(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 all cm handles by dmi plugin identifier'() { + given: 'the DataNodes queried for a given cpsPath are returned from the persistence service.' + mockResponses() + when: 'cm Handles are fetched for a given dmi plugin identifier' + def result = objectUnderTest.getCmHandleIdsByDmiPluginIdentifier('my-dmi-plugin-identifier') + then: 'result is the correct size' + assert result.size() == 3 + and: 'result contains the correct cm handles' + assert result.containsAll('PNFDemo', 'PNFDemo2', 'PNFDemo4') + } + + 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] + cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@dmi-service-name=\'my-dmi-plugin-identifier\']', OMIT_DESCENDANTS) >> [pnfDemo, pnfDemo2] + cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@dmi-data-service-name=\'my-dmi-plugin-identifier\']', OMIT_DESCENDANTS) >> [pnfDemo, pnfDemo4] + cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@dmi-model-service-name=\'my-dmi-plugin-identifier\']', OMIT_DESCENDANTS) >> [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/impl/inventory/CompositeStateBuilderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/CompositeStateBuilderSpec.groovy new file mode 100644 index 0000000000..777b505b46 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/CompositeStateBuilderSpec.groovy @@ -0,0 +1,92 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Bell Canada + * Modifications Copyright (C) 2022-2023 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.impl.inventory + +import org.onap.cps.ncmp.api.impl.inventory.CmHandleState +import org.onap.cps.ncmp.api.impl.inventory.CompositeState +import org.onap.cps.ncmp.api.impl.inventory.CompositeStateBuilder +import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState +import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory +import org.onap.cps.spi.model.DataNode +import org.onap.cps.spi.model.DataNodeBuilder +import spock.lang.Specification + +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter + +class CompositeStateBuilderSpec extends Specification { + + def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ") + .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC)) + + def static cmHandleId = 'myHandle1' + def static cmHandleXpath = "/dmi-registry/cm-handles[@id='${cmHandleId}/state']" + def static stateDataNodes = [new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/state/lock-reason") + .withLeaves(['reason': 'MODULE_SYNC_FAILED', 'details': 'lock details']).build(), + new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/state/datastores") + .withChildDataNodes(Arrays.asList(new DataNodeBuilder() + .withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/state/datastores/operational") + .withLeaves(['sync-state': 'UNSYNCHRONIZED']).build())).build()] + def static cmHandleDataNode = new DataNode(xpath: cmHandleXpath, childDataNodes: stateDataNodes, leaves: ['cm-handle-state': 'ADVISED']) + + def "Composite State Specification"() { + when: 'using composite state builder ' + def compositeState = new CompositeStateBuilder().withCmHandleState(CmHandleState.ADVISED) + .withLockReason(LockReasonCategory.MODULE_SYNC_FAILED,"").withOperationalDataStores(DataStoreSyncState.UNSYNCHRONIZED, + formattedDateAndTime.toString()).withLastUpdatedTime(formattedDateAndTime).build() + then: 'it matches expected cm handle state and data store sync state' + assert compositeState.cmHandleState == CmHandleState.ADVISED + assert compositeState.dataStores.operationalDataStore.dataStoreSyncState == DataStoreSyncState.UNSYNCHRONIZED + } + + def "Build composite state from DataNode "() { + given: "a Data Node " + new DataNode(leaves: ['cm-handle-state': 'ADVISED']) + when: 'build from data node function is invoked' + def compositeState = new CompositeStateBuilder().fromDataNode(cmHandleDataNode).build() + then: 'it matches expected state model as JSON' + assert compositeState.cmHandleState == CmHandleState.ADVISED + } + + def 'CompositeStateBuilder build'() { + given: 'A CompositeStateBuilder with all private fields set' + def finalCompositeStateBuilder = new CompositeStateBuilder() + .withCmHandleState(CmHandleState.ADVISED) + .withLastUpdatedTime(formattedDateAndTime.toString()) + .withLockReason(LockReasonCategory.MODULE_SYNC_FAILED, 'locked details') + .withOperationalDataStores(DataStoreSyncState.SYNCHRONIZED, formattedDateAndTime) + when: 'build is called' + def result = finalCompositeStateBuilder.build() + then: 'result is of the correct type' + assert result.class == CompositeState.class + and: 'built result should have correct values' + assert !result.getDataSyncEnabled() + assert result.getLastUpdateTime() == formattedDateAndTime + assert result.getLockReason().getLockReasonCategory() == LockReasonCategory.MODULE_SYNC_FAILED + assert result.getLockReason().getDetails() == 'locked details' + assert result.getCmHandleState() == CmHandleState.ADVISED + assert result.getDataStores().getOperationalDataStore().getDataStoreSyncState() == DataStoreSyncState.SYNCHRONIZED + assert result.getDataStores().getOperationalDataStore().getLastSyncTime() == formattedDateAndTime + } + +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/CompositeStateSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/CompositeStateSpec.groovy new file mode 100644 index 0000000000..d8beba8de0 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/CompositeStateSpec.groovy @@ -0,0 +1,64 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2023 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.impl.inventory + +import com.fasterxml.jackson.databind.ObjectMapper +import org.onap.cps.ncmp.api.impl.inventory.CmHandleState +import org.onap.cps.ncmp.api.impl.inventory.CompositeState +import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState +import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory +import spock.lang.Specification +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter + +import static org.onap.cps.ncmp.api.impl.inventory.CompositeState.DataStores +import static org.onap.cps.ncmp.api.impl.inventory.CompositeState.Operational +import static org.onap.cps.ncmp.utils.TestUtils.getResourceFileContent +import static org.springframework.util.StringUtils.trimAllWhitespace + +class CompositeStateSpec extends Specification { + + def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ") + .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC)) + def objectMapper = new ObjectMapper() + + def "Composite State Specification"() { + given: "a Composite State" + def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, + lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.MODULE_SYNC_FAILED).details("lock details").build(), + lastUpdateTime: formattedDateAndTime.toString(), + dataSyncEnabled: false, + dataStores: dataStores()) + when: 'it is represented as JSON' + def resultJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(compositeState) + then: 'it matches expected state model as JSON' + def expectedJson = getResourceFileContent('expectedStateModel.json') + assert trimAllWhitespace(expectedJson) == trimAllWhitespace(resultJson) + } + + def dataStores() { + DataStores.builder().operationalDataStore(Operational.builder() + .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED) + .lastSyncTime(formattedDateAndTime.toString()).build()) + .build() + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImplSpec.groovy new file mode 100644 index 0000000000..bb4eebd40e --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImplSpec.groovy @@ -0,0 +1,311 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2023 Nordix Foundation + * Modifications Copyright (C) 2022 Bell Canada + * Modifications Copyright (C) 2023 TechMahindra Ltd. + * ================================================================================ + * 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.impl.inventory + +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 +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.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NO_TIMESTAMP +import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS + +import com.fasterxml.jackson.databind.ObjectMapper +import org.onap.cps.api.CpsAdminService +import org.onap.cps.api.CpsDataService +import org.onap.cps.api.CpsModuleService +import org.onap.cps.ncmp.api.impl.inventory.CmHandleState +import org.onap.cps.ncmp.api.impl.inventory.CompositeState +import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistenceImpl +import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle +import org.onap.cps.spi.CascadeDeleteAllowed +import org.onap.cps.spi.FetchDescendantsOption +import org.onap.cps.spi.model.DataNode +import org.onap.cps.spi.model.ModuleDefinition +import org.onap.cps.spi.model.ModuleReference +import org.onap.cps.utils.JsonObjectMapper +import org.onap.cps.spi.utils.CpsValidator +import spock.lang.Shared +import spock.lang.Specification +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter + +class InventoryPersistenceImplSpec extends Specification { + + def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper())) + + def mockCpsDataService = Mock(CpsDataService) + + def mockCpsModuleService = Mock(CpsModuleService) + + def mockCpsAdminService = Mock(CpsAdminService) + + def mockCpsValidator = Mock(CpsValidator) + + def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService, + mockCpsValidator, mockCpsAdminService) + + def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ") + .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC)) + + def cmHandleId = 'some-cm-handle' + def leaves = ["dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"] + def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']" + + def cmHandleId2 = 'another-cm-handle' + def xpath2 = "/dmi-registry/cm-handles[@id='another-cm-handle']" + + @Shared + def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]), + new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])] + + @Shared + def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])] + + @Shared + def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])] + + @Shared + def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])] + + 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) + mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode] + when: 'retrieving the yang modelled cm handle' + def result = objectUnderTest.getYangModelCmHandle(cmHandleId) + then: 'the result has the correct id and service names' + result.id == cmHandleId + result.dmiServiceName == 'common service name' + result.dmiDataServiceName == 'data service name' + result.dmiModelServiceName == 'model service name' + and: 'the expected DMI properties' + result.dmiProperties == expectedDmiProperties + result.publicProperties == expectedPublicProperties + and: 'the state details are returned' + result.compositeState.cmHandleState == expectedCompositeState + and: 'the CM Handle ID is validated' + 1 * mockCpsValidator.validateNameCharacters(cmHandleId) + where: 'the following parameters are used' + scenario | childDataNodes || expectedDmiProperties || expectedPublicProperties || expectedCompositeState + 'no properties' | [] || [] || [] || null + 'DMI and public properties' | childDataNodesForCmHandleWithAllProperties || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")] || null + 'just DMI properties' | childDataNodesForCmHandleWithDMIProperties || [new YangModelCmHandle.Property("name1", "value1")] || [] || null + 'just public properties' | childDataNodesForCmHandleWithPublicProperties || [] || [new YangModelCmHandle.Property("name2", "value2")] || null + 'with state details' | childDataNodesForCmHandleWithState || [] || [] || CmHandleState.ADVISED + } + + def "Handling missing service names as null."() { + given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes' + def dataNode = new DataNode(childDataNodes:[], leaves: [:]) + mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode] + when: 'retrieving the yang modelled cm handle' + def result = objectUnderTest.getYangModelCmHandle(cmHandleId) + then: 'the service names are returned as null' + result.dmiServiceName == null + result.dmiDataServiceName == null + result.dmiModelServiceName == null + and: 'the CM Handle ID is validated' + 1 * mockCpsValidator.validateNameCharacters(cmHandleId) + } + + def "Retrieve multiple YangModelCmHandles"() { + given: 'the cps data service returns 2 data nodes from the DMI registry' + def dataNodes = [new DataNode(xpath: xpath), new DataNode(xpath: xpath2)] + mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes + when: 'retrieving the yang modelled cm handle' + def results = objectUnderTest.getYangModelCmHandles([cmHandleId, cmHandleId2]) + then: 'verify both have returned and cmhandleIds are correct' + assert results.size() == 2 + assert results.id.containsAll([cmHandleId, cmHandleId2]) + } + + def 'Get a Cm Handle Composite State'() { + given: 'a valid cm handle id' + def cmHandleId = 'Some-Cm-Handle' + def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED']) + and: 'cps data service returns a valid data node' + mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode] + when: 'get cm handle state is invoked' + def result = objectUnderTest.getCmHandleState(cmHandleId) + then: 'result has returned the correct cm handle state' + result.cmHandleState == CmHandleState.ADVISED + and: 'the CM Handle ID is validated' + 1 * mockCpsValidator.validateNameCharacters(cmHandleId) + } + + def 'Update Cm Handle with #scenario State'() { + given: 'a cm handle and a composite state' + def cmHandleId = 'Some-Cm-Handle' + def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime) + when: 'update cm handle state is invoked with the #scenario state' + objectUnderTest.saveCmHandleState(cmHandleId, compositeState) + then: 'update node leaves is invoked with the correct params' + 1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime) + where: 'the following states are used' + scenario | cmHandleState || expectedJsonData + 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}' + 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}' + 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}' + } + + def 'Update Cm Handles with #scenario States'() { + given: 'a map of cm handles composite states' + def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime) + def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime) + when: 'update cm handle state is invoked with the #scenario state' + def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2] + objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap) + then: 'update node leaves is invoked with the correct params' + 1 * mockCpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandlesJsonDataMap, _ as OffsetDateTime) + where: 'the following states are used' + scenario | cmHandleState || cmHandlesJsonDataMap + 'READY' | CmHandleState.READY || ['/dmi-registry/cm-handles[@id=\'Some-Cm-Handle1\']':'{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle2\']':'{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'] + 'LOCKED' | CmHandleState.LOCKED || ['/dmi-registry/cm-handles[@id=\'Some-Cm-Handle1\']':'{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle2\']':'{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'] + 'DELETING' | CmHandleState.DELETING || ['/dmi-registry/cm-handles[@id=\'Some-Cm-Handle1\']':'{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle2\']':'{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'] + } + + def 'Get module definitions'() { + given: 'cps module service returns a collection of module definitions' + def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')] + mockCpsModuleService.getModuleDefinitionsByAnchorName(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleDefinitions + when: 'get module definitions by cmHandle is invoked' + def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id') + then: 'the returned result are the same module definitions as returned from the module service' + assert result == moduleDefinitions + } + + def 'Get module references'() { + given: 'cps module service returns a collection of module references' + def moduleReferences = [new ModuleReference('moduleName','revision','namespace')] + mockCpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleReferences + when: 'get yang resources module references by cmHandle is invoked' + def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id') + then: 'the returned result is a collection of module definitions' + assert result == moduleReferences + and: 'the CM Handle ID is validated' + 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id') + } + + def 'Save Cmhandle'() { + given: 'cmHandle represented as Yang Model' + def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: []) + when: 'the method to save cmhandle is called' + objectUnderTest.saveCmHandle(yangModelCmHandle) + then: 'the data service method to save list elements is called once' + 1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT, + _,null) >> { + args -> { + assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}') + } + } + } + + def 'Save Multiple Cmhandles'() { + given: 'cm handles represented as Yang Model' + def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1') + def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2') + when: 'the cm handles are saved' + objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2]) + then: 'CPS Data Service persists both cm handles as a batch' + 1 * mockCpsDataService.saveListElementsBatch(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + NCMP_DMI_REGISTRY_PARENT, _,null) >> { + args -> { + def jsonDataList = (args[3] as List) + (jsonDataList[0] as String).contains('cmhandle1') + (jsonDataList[0] as String).contains('cmhandle2') + } + } + } + + def 'Delete list or list elements'() { + when: 'the method to delete list or list elements is called' + objectUnderTest.deleteListOrListElement('sample xPath') + then: 'the data service method to save list elements is called once' + 1 * mockCpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath',null) + } + + def 'Delete schema set with a valid schema set name'() { + when: 'the method to delete schema set is called with valid schema set name' + objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName') + then: 'the module service to delete schemaSet is invoked once' + 1 * mockCpsModuleService.deleteSchemaSet(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED) + and: 'the schema set name is validated' + 1 * mockCpsValidator.validateNameCharacters('validSchemaSetName') + } + + def 'Delete multiple schema sets with valid schema set names'() { + when: 'the method to delete schema sets is called with valid schema set names' + objectUnderTest.deleteSchemaSetsWithCascade(['validSchemaSetName1', 'validSchemaSetName2']) + then: 'the module service to delete schema sets is invoked once' + 1 * mockCpsModuleService.deleteSchemaSetsWithCascade(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['validSchemaSetName1', 'validSchemaSetName2']) + and: 'the schema set names are validated' + 1 * mockCpsValidator.validateNameCharacters(['validSchemaSetName1', 'validSchemaSetName2']) + } + + def 'Get data node via xPath'() { + when: 'the method to get data nodes is called' + objectUnderTest.getDataNode('sample xPath') + then: 'the data persistence service method to get data node is invoked once' + 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath', INCLUDE_ALL_DESCENDANTS) + } + + def 'Get cmHandle data node'() { + given: 'expected xPath to get cmHandle data node' + def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']'; + when: 'the method to get data nodes is called' + objectUnderTest.getCmHandleDataNode('sample cmHandleId') + then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath' + 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, expectedXPath, INCLUDE_ALL_DESCENDANTS) + } + + def 'Get CM handles that has given module names'() { + when: 'the method to get cm handles is called' + objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name']) + then: 'the admin persistence service method to query anchors is invoked once with the same parameter' + 1 * mockCpsAdminService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name']) + } + + def 'Replace list content'() { + when: 'replace list content method is called with xpath and data nodes collection' + objectUnderTest.replaceListContent('sample xpath', [new DataNode()]) + then: 'the cps data service method to replace list content is invoked once with same parameters' + 1 * mockCpsDataService.replaceListContent(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xpath', [new DataNode()], NO_TIMESTAMP); + } + + def 'Delete data node via xPath'() { + when: 'Delete data node method is called with xpath as parameter' + objectUnderTest.deleteDataNode('sample dataNode xpath') + then: 'the cps data service method to delete data node is invoked once with the same xPath' + 1 * mockCpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'sample dataNode xpath', NO_TIMESTAMP); + } + + def 'Delete multiple data nodes via xPath'() { + when: 'Delete data nodes method is called with multiple xpaths as parameters' + objectUnderTest.deleteDataNodes(['xpath1', 'xpath2']) + then: 'the cps data service method to delete data nodes is invoked once with the same xPaths' + 1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP); + } + +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/DataSyncWatchdogSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/DataSyncWatchdogSpec.groovy new file mode 100644 index 0000000000..65dfc05791 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/DataSyncWatchdogSpec.groovy @@ -0,0 +1,118 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2023 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.impl.inventory.sync + +import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME + +import com.hazelcast.map.IMap +import org.onap.cps.api.CpsDataService +import org.onap.cps.ncmp.api.impl.inventory.sync.DataSyncWatchdog +import org.onap.cps.ncmp.api.impl.inventory.sync.SyncUtils +import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle +import org.onap.cps.ncmp.api.impl.inventory.CmHandleState +import org.onap.cps.ncmp.api.impl.inventory.CompositeState +import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence +import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState +import spock.lang.Specification + +class DataSyncWatchdogSpec extends Specification { + + def mockInventoryPersistence = Mock(InventoryPersistence) + + def mockCpsDataService = Mock(CpsDataService) + + def mockSyncUtils = Mock(SyncUtils) + + def mockDataSyncSemaphores = Mock(IMap) + + def jsonString = '{"stores:bookstore":{"categories":[{"code":"01"}]}}' + + def objectUnderTest = new DataSyncWatchdog(mockInventoryPersistence, mockCpsDataService, mockSyncUtils, mockDataSyncSemaphores) + + def compositeState = getCompositeState() + + def yangModelCmHandle1 = createSampleYangModelCmHandle('cm-handle-1') + + def yangModelCmHandle2 = createSampleYangModelCmHandle('cm-handle-2') + + def 'Data Sync for Cm Handle State in READY and Operational Sync State in UNSYNCHRONIZED.'() { + given: 'sample resource data' + def resourceData = jsonString + and: 'sync utilities returns a cm handle twice' + mockSyncUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1, yangModelCmHandle2] + when: 'data sync poll is executed' + objectUnderTest.executeUnSynchronizedReadyCmHandlePoll() + then: 'the inventory persistence cm handle returns a composite state for the first cm handle' + 1 * mockInventoryPersistence.getCmHandleState('cm-handle-1') >> compositeState + and: 'the sync util returns first resource data' + 1 * mockSyncUtils.getResourceData('cm-handle-1') >> resourceData + and: 'the cm-handle data is saved' + 1 * mockCpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cm-handle-1', jsonString, _) + and: 'the first cm handle operational sync state is updated' + 1 * mockInventoryPersistence.saveCmHandleState('cm-handle-1', compositeState) + then: 'the inventory persistence cm handle returns a composite state for the second cm handle' + 1 * mockInventoryPersistence.getCmHandleState('cm-handle-2') >> compositeState + and: 'the sync util returns first resource data' + 1 * mockSyncUtils.getResourceData('cm-handle-2') >> resourceData + and: 'the cm-handle data is saved' + 1 * mockCpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cm-handle-2', jsonString, _) + and: 'the second cm handle operational sync state is updated from "UNSYNCHRONIZED" to "SYNCHRONIZED"' + 1 * mockInventoryPersistence.saveCmHandleState('cm-handle-2', compositeState) + } + + def 'Data Sync for Cm Handle State in READY and Operational Sync State in UNSYNCHRONIZED without resource data.'() { + given: 'sync utilities returns a cm handle' + mockSyncUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1] + when: 'data sync poll is executed' + objectUnderTest.executeUnSynchronizedReadyCmHandlePoll() + then: 'the inventory persistence cm handle returns a composite state for the first cm handle' + 1 * mockInventoryPersistence.getCmHandleState('cm-handle-1') >> compositeState + and: 'the sync util returns no resource data' + 1 * mockSyncUtils.getResourceData('cm-handle-1') >> null + and: 'the cm-handle data is not saved' + 0 * mockCpsDataService.saveData(*_) + } + + def 'Data Sync for Cm Handle that is already being processed.'() { + given: 'sync utilities returns a cm handle' + mockSyncUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1] + and: 'the shared data sync semaphore indicate it is already being processed' + mockDataSyncSemaphores.putIfAbsent('cm-handle-1', _, _, _) >> 'something (not null)' + when: 'data sync poll is executed' + objectUnderTest.executeUnSynchronizedReadyCmHandlePoll() + then: 'it is NOT processed e.g. state is not requested' + 0 * mockInventoryPersistence.getCmHandleState(*_) + } + + def createSampleYangModelCmHandle(cmHandleId) { + def compositeState = getCompositeState() + return new YangModelCmHandle(id: cmHandleId, compositeState: compositeState) + } + + def getCompositeState() { + def cmHandleState = CmHandleState.READY + def compositeState = new CompositeState(cmHandleState: cmHandleState) + compositeState.setDataStores(CompositeState.DataStores.builder() + .operationalDataStore(CompositeState.Operational.builder().dataStoreSyncState(DataStoreSyncState.SYNCHRONIZED) + .build()).build()) + return compositeState + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncServiceSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncServiceSpec.groovy new file mode 100644 index 0000000000..18b87af9a1 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncServiceSpec.groovy @@ -0,0 +1,187 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2023 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.impl.inventory.sync + +import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.LOCKED_MISBEHAVING +import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME +import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_UPGRADE + +import org.onap.cps.ncmp.api.impl.inventory.CmHandleState +import org.onap.cps.spi.FetchDescendantsOption +import org.onap.cps.spi.model.DataNode +import org.onap.cps.api.CpsAdminService +import org.onap.cps.api.CpsDataService +import org.onap.cps.api.CpsModuleService +import org.onap.cps.spi.model.DataNodeBuilder +import org.onap.cps.ncmp.api.impl.inventory.sync.ModuleSyncService +import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations +import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle +import org.onap.cps.ncmp.api.impl.inventory.CmHandleQueries +import org.onap.cps.ncmp.api.impl.inventory.CompositeStateBuilder +import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle +import org.onap.cps.spi.CascadeDeleteAllowed +import org.onap.cps.spi.exceptions.SchemaSetNotFoundException +import org.onap.cps.spi.model.ModuleReference +import org.onap.cps.utils.JsonObjectMapper +import spock.lang.Specification + +class ModuleSyncServiceSpec extends Specification { + + def mockCpsModuleService = Mock(CpsModuleService) + def mockDmiModelOperations = Mock(DmiModelOperations) + def mockCpsAdminService = Mock(CpsAdminService) + def mockCmHandleQueries = Mock(CmHandleQueries) + def mockCpsDataService = Mock(CpsDataService) + def mockJsonObjectMapper = Mock(JsonObjectMapper) + def mockModuleSetTagCache = [:] + + def objectUnderTest = new ModuleSyncService(mockDmiModelOperations, mockCpsModuleService, mockCpsAdminService, + mockCmHandleQueries, mockCpsDataService, mockJsonObjectMapper, mockModuleSetTagCache) + + def expectedDataspaceName = NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME + def static cmHandleWithModuleSetTag = new DataNodeBuilder().withXpath("//cm-handles[@module-set-tag='tag-1'][@id='otherId']").withAnchor('otherId').build() + + def 'Sync model for a NEW cm handle without cache with #scenario'() { + given: 'a cm handle having some state' + def ncmpServiceCmHandle = new NcmpServiceCmHandle() + ncmpServiceCmHandle.setCompositeState(new CompositeStateBuilder().withCmHandleState(CmHandleState.ADVISED).build()) + ncmpServiceCmHandle.cmHandleId = 'ch-1' + def yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle('some service name', '', '', ncmpServiceCmHandle, moduleSetTag) + and: 'DMI operations returns some module references' + def moduleReferences = [ new ModuleReference('module1','1'), new ModuleReference('module2','2') ] + mockDmiModelOperations.getModuleReferences(yangModelCmHandle) >> moduleReferences + and: 'DMI-Plugin returns resource(s) for "new" module(s)' + mockDmiModelOperations.getNewYangResourcesFromDmi(yangModelCmHandle, [new ModuleReference('module1', '1')]) >> newModuleNameContentToMap + and: 'the module service identifies #identifiedNewModuleReferences.size() new modules' + mockCpsModuleService.identifyNewModuleReferences(moduleReferences) >> toModuleReferences(identifiedNewModuleReferences) + and: 'system contains #existingCmHandlesWithSameTag.size() cm handles with same tag' + mockCmHandleQueries.queryNcmpRegistryByCpsPath("//cm-handles[@module-set-tag='new-tag-1']", + FetchDescendantsOption.OMIT_DESCENDANTS) >> [] + when: 'module sync is triggered' + objectUnderTest.syncAndCreateOrUpgradeSchemaSetAndAnchor(yangModelCmHandle) + then: 'create schema set from module is invoked with correct parameters' + 1 * mockCpsModuleService.createOrUpgradeSchemaSetFromModules(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'ch-1', newModuleNameContentToMap, moduleReferences) + and: 'anchor is created with the correct parameters' + 1 * mockCpsAdminService.createAnchor(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'ch-1', 'ch-1') + where: 'the following parameters are used' + scenario | existingModuleResourcesInCps | identifiedNewModuleReferences | newModuleNameContentToMap | moduleSetTag + 'one new module' | [['module2': '2'], ['module3': '3']] | [['module1': '1']] | [module1: 'some yang source'] | '' + 'no new module' | [['module1': '1'], ['module2': '2']] | [] | [:] | 'new-tag-1' + } + + def 'Upgrade model for an existing cm handle with Module Set Tag where the modules are #scenario'() { + given: 'a cm handle being upgraded to module set tag: tag-1' + def ncmpServiceCmHandle = new NcmpServiceCmHandle() + ncmpServiceCmHandle.setCompositeState(new CompositeStateBuilder().withLockReason(MODULE_UPGRADE, 'new moduleSetTag: tag-1').build()) + def dmiServiceName = 'some service name' + ncmpServiceCmHandle.cmHandleId = 'upgraded-ch' + def yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle(dmiServiceName, '', '', ncmpServiceCmHandle,'tag-1') + and: 'some module references' + def moduleReferences = [ new ModuleReference('module1','1') ] + and: 'cache or DMI operations returns some module references for upgraded cm handle' + if (populateCache) { + mockModuleSetTagCache.put('tag-1',moduleReferences) + } else { + mockDmiModelOperations.getModuleReferences(yangModelCmHandle) >> moduleReferences + } + and: 'none of these module references are new (unknown to the system)' + mockCpsModuleService.identifyNewModuleReferences(moduleReferences) >> [] + and: 'CPS-Core returns list of existing module resources for TBD' + mockCpsModuleService.getYangResourcesModuleReferences(*_) >> [ new ModuleReference('module1','1') ] + and: 'system contains #existingCmHandlesWithSameTag.size() cm handles with same tag' + mockCmHandleQueries.queryNcmpRegistryByCpsPath("//cm-handles[@module-set-tag='tag-1']", FetchDescendantsOption.OMIT_DESCENDANTS) >> existingCmHandlesWithSameTag + and: 'the other cm handle is a state ready' + mockCmHandleQueries.cmHandleHasState('otherId', CmHandleState.READY) >> true + when: 'module sync is triggered' + objectUnderTest.syncAndCreateOrUpgradeSchemaSetAndAnchor(yangModelCmHandle) + then: 'create schema set from module is invoked for the upgraded cm handle' + 1 * mockCpsModuleService.createOrUpgradeSchemaSetFromModules(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'upgraded-ch', [:], moduleReferences) + and: 'No anchor is created for the upgraded cm handle' + 0 * mockCpsAdminService.createAnchor(*_) + where: 'the following parameters are used' + scenario | populateCache | existingCmHandlesWithSameTag + 'new' | false | [] + 'in cache' | true | [] + 'in database' | false | [cmHandleWithModuleSetTag] + } + + def 'upgrade model for a existing cm handle'() { + given: 'a cm handle that is ready but locked for upgrade' + def ncmpServiceCmHandle = new NcmpServiceCmHandle() + ncmpServiceCmHandle.setCompositeState(new CompositeStateBuilder() + .withLockReason(MODULE_UPGRADE, 'new moduleSetTag: targetModuleSetTag').build()) + ncmpServiceCmHandle.setCmHandleId('cmHandleId-1') + def yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle('some service name', '', '', ncmpServiceCmHandle, 'targetModuleSetTag') + mockCmHandleQueries.cmHandleHasState('cmHandleId-1', CmHandleState.READY) >> true + and: 'the module service returns some module references' + def moduleReferences = [new ModuleReference('module1', '1'), new ModuleReference('module2', '2')] + mockCpsModuleService.getYangResourcesModuleReferences(*_)>> moduleReferences + and: 'a cm handle with the same moduleSetTag can be found in the registry' + mockCmHandleQueries.queryNcmpRegistryByCpsPath("//cm-handles[@module-set-tag='targetModuleSetTag']", + FetchDescendantsOption.OMIT_DESCENDANTS) >> [new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'cmHandleId-1\']', leaves: ['id': 'cmHandleId-1', 'cm-handle-state': 'READY'])] + when: 'module upgrade is triggered' + objectUnderTest.syncAndCreateOrUpgradeSchemaSetAndAnchor(yangModelCmHandle) + then: 'the upgrade is delegated to the module service (with the correct parameters)' + 1 * mockCpsModuleService.createOrUpgradeSchemaSetFromModules(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cmHandleId-1', Collections.emptyMap(), moduleReferences) + } + + def 'Delete Schema Set for CmHandle'() { + when: 'delete schema set if exists is called' + objectUnderTest.deleteSchemaSetIfExists('some-cmhandle-id') + then: 'the module service is invoked to delete the correct schema set' + 1 * mockCpsModuleService.deleteSchemaSet(expectedDataspaceName, 'some-cmhandle-id', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED) + } + + def 'Delete a non-existing Schema Set for CmHandle' () { + given: 'the DB throws an exception because its Schema Set does not exist' + mockCpsModuleService.deleteSchemaSet(*_) >> { throw new SchemaSetNotFoundException('some-dataspace-name', 'some-cmhandle-id') } + when: 'delete schema set if exists is called' + objectUnderTest.deleteSchemaSetIfExists('some-cmhandle-id') + then: 'the exception from the DB is ignored; there are no exceptions' + noExceptionThrown() + } + + def 'Delete Schema Set for CmHandle with other exception' () { + given: 'an exception other than SchemaSetNotFoundException is thrown' + UnsupportedOperationException unsupportedOperationException = new UnsupportedOperationException(); + 1 * mockCpsModuleService.deleteSchemaSet(*_) >> { throw unsupportedOperationException } + when: 'delete schema set if exists is called' + objectUnderTest.deleteSchemaSetIfExists('some-cmhandle-id') + then: 'an exception is thrown' + def result = thrown(UnsupportedOperationException) + result == unsupportedOperationException + } + + def 'Extract module set tag'() { + expect: 'the module set tag is extracted correctly' + assert 'targetModuleSetTag' == objectUnderTest.extractModuleSetTag(new CompositeStateBuilder().withLockReason(MODULE_UPGRADE, 'new moduleSetTag: targetModuleSetTag').build()) + } + + def toModuleReferences(moduleReferenceAsMap) { + def moduleReferences = [].withDefault { [:] } + moduleReferenceAsMap.forEach(property -> + property.forEach((moduleName, revision) -> { + moduleReferences.add(new ModuleReference('moduleName' : moduleName, 'revision' : revision)) + })) + return moduleReferences + } + +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncTasksSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncTasksSpec.groovy new file mode 100644 index 0000000000..0d927551d0 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncTasksSpec.groovy @@ -0,0 +1,217 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2023 Nordix Foundation + * 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. + * 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.impl.inventory.sync + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender +import com.hazelcast.config.Config +import com.hazelcast.instance.impl.HazelcastInstanceFactory +import com.hazelcast.map.IMap +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.onap.cps.ncmp.api.impl.events.lcm.LcmEventsCmHandleStateHandler +import org.onap.cps.ncmp.api.impl.inventory.sync.ModuleSyncService +import org.onap.cps.ncmp.api.impl.inventory.sync.ModuleSyncTasks +import org.onap.cps.ncmp.api.impl.inventory.sync.SyncUtils +import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle +import org.onap.cps.ncmp.api.impl.inventory.CmHandleState +import org.onap.cps.ncmp.api.impl.inventory.CompositeState +import org.onap.cps.ncmp.api.impl.inventory.CompositeStateBuilder +import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence +import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory +import org.onap.cps.spi.model.DataNode +import org.slf4j.LoggerFactory +import spock.lang.Specification +import java.util.concurrent.atomic.AtomicInteger + +class ModuleSyncTasksSpec extends Specification { + + def logger = Spy(ListAppender) + + @BeforeEach + void setup() { + ((Logger) LoggerFactory.getLogger(ModuleSyncTasks.class)).addAppender(logger); + logger.start(); + } + + @AfterEach + void teardown() { + ((Logger) LoggerFactory.getLogger(ModuleSyncTasks.class)).detachAndStopAllAppenders(); + } + + def mockInventoryPersistence = Mock(InventoryPersistence) + + def mockSyncUtils = Mock(SyncUtils) + + def mockModuleSyncService = Mock(ModuleSyncService) + + def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler) + + IMap moduleSyncStartedOnCmHandles = HazelcastInstanceFactory + .getOrCreateHazelcastInstance(new Config('hazelcastInstanceName')) + .getMap('mapInstanceName') + + def batchCount = new AtomicInteger(5) + + def objectUnderTest = new ModuleSyncTasks(mockInventoryPersistence, mockSyncUtils, mockModuleSyncService, + mockLcmEventsCmHandleStateHandler, moduleSyncStartedOnCmHandles) + + def 'Module Sync ADVISED cm handles.'() { + given: 'cm handles in an ADVISED state' + def cmHandle1 = advisedCmHandleAsDataNode('cm-handle-1') + def cmHandle2 = advisedCmHandleAsDataNode('cm-handle-2') + and: 'the inventory persistence cm handle returns a ADVISED state for the any handle' + mockInventoryPersistence.getCmHandleState(_) >> new CompositeState(cmHandleState: CmHandleState.ADVISED) + when: 'module sync poll is executed' + objectUnderTest.performModuleSync([cmHandle1, cmHandle2], batchCount) + then: 'module sync service deletes schemas set of each cm handle if it already exists' + 1 * mockModuleSyncService.deleteSchemaSetIfExists('cm-handle-1') + 1 * mockModuleSyncService.deleteSchemaSetIfExists('cm-handle-2') + and: 'module sync service is invoked for each cm handle' + 1 * mockModuleSyncService.syncAndCreateOrUpgradeSchemaSetAndAnchor(_) >> { args -> assertYamgModelCmHandleArgument(args, 'cm-handle-1') } + 1 * mockModuleSyncService.syncAndCreateOrUpgradeSchemaSetAndAnchor(_) >> { args -> assertYamgModelCmHandleArgument(args, 'cm-handle-2') } + and: 'the state handler is called for the both cm handles' + 1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >> { args -> + assertBatch(args, ['cm-handle-1', 'cm-handle-2'], CmHandleState.READY) + } + and: 'batch count is decremented by one' + assert batchCount.get() == 4 + } + + def 'Module Sync ADVISED cm handle with failure during sync.'() { + given: 'a cm handle in an ADVISED state' + def cmHandle = advisedCmHandleAsDataNode('cm-handle') + and: 'the inventory persistence cm handle returns a ADVISED state for the cm handle' + def cmHandleState = new CompositeState(cmHandleState: CmHandleState.ADVISED) + 1 * mockInventoryPersistence.getCmHandleState('cm-handle') >> cmHandleState + and: 'module sync service attempts to sync the cm handle and throws an exception' + 1 * mockModuleSyncService.syncAndCreateOrUpgradeSchemaSetAndAnchor(*_) >> { throw new Exception('some exception') } + when: 'module sync is executed' + objectUnderTest.performModuleSync([cmHandle], batchCount) + then: 'update lock reason, details and attempts is invoked' + 1 * mockSyncUtils.updateLockReasonDetailsAndAttempts(cmHandleState, LockReasonCategory.MODULE_SYNC_FAILED, 'some exception') + and: 'the state handler is called to update the state to LOCKED' + 1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >> { args -> + assertBatch(args, ['cm-handle'], CmHandleState.LOCKED) + } + and: 'batch count is decremented by one' + assert batchCount.get() == 4 + } + + def 'Reset failed CM Handles #scenario.'() { + given: 'cm handles in an locked state' + def lockedState = new CompositeStateBuilder().withCmHandleState(CmHandleState.LOCKED) + .withLockReason(LockReasonCategory.MODULE_SYNC_FAILED, '').withLastUpdatedTimeNow().build() + def yangModelCmHandle1 = new YangModelCmHandle(id: 'cm-handle-1', compositeState: lockedState) + def yangModelCmHandle2 = new YangModelCmHandle(id: 'cm-handle-2', compositeState: lockedState) + def expectedCmHandleStatePerCmHandle = [(yangModelCmHandle1): CmHandleState.ADVISED] + and: 'clear in progress map' + resetModuleSyncStartedOnCmHandles(moduleSyncStartedOnCmHandles) + and: 'add cm handle entry into progress map' + moduleSyncStartedOnCmHandles.put('cm-handle-1', 'started') + moduleSyncStartedOnCmHandles.put('cm-handle-2', 'started') + and: 'sync utils retry locked cm handle returns #isReadyForRetry' + mockSyncUtils.needsModuleSyncRetryOrUpgrade(lockedState) >>> isReadyForRetry + when: 'resetting failed cm handles' + objectUnderTest.resetFailedCmHandles([yangModelCmHandle1, yangModelCmHandle2]) + then: 'updated to state "ADVISED" from "READY" is called as often as there are cm handles ready for retry' + expectedNumberOfInvocationsToUpdateCmHandleState * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(expectedCmHandleStatePerCmHandle) + and: 'after reset performed size of in progress map' + assert moduleSyncStartedOnCmHandles.size() == inProgressMapSize + where: + scenario | isReadyForRetry | inProgressMapSize || expectedNumberOfInvocationsToUpdateCmHandleState + 'retry locked cm handle' | [true, false] | 1 || 1 + 'do not retry locked cm handle' | [false, false] | 2 || 0 + } + + def 'Module Sync ADVISED cm handle without entry in progress map.'() { + given: 'cm handles in an ADVISED state' + def cmHandle1 = advisedCmHandleAsDataNode('cm-handle-1') + and: 'the inventory persistence cm handle returns a ADVISED state for the any handle' + mockInventoryPersistence.getCmHandleState(_) >> new CompositeState(cmHandleState: CmHandleState.ADVISED) + and: 'entry in progress map for other cm handle' + moduleSyncStartedOnCmHandles.put('other-cm-handle', 'started') + when: 'module sync poll is executed' + objectUnderTest.performModuleSync([cmHandle1], batchCount) + then: 'module sync service is invoked for cm handle' + 1 * mockModuleSyncService.syncAndCreateOrUpgradeSchemaSetAndAnchor(_) >> { args -> assertYamgModelCmHandleArgument(args, 'cm-handle-1') } + and: 'the entry for other cm handle is still in the progress map' + assert moduleSyncStartedOnCmHandles.get('other-cm-handle') != null + } + + def 'Remove already processed cm handle id from hazelcast map'() { + given: 'hazelcast map contains cm handle id' + moduleSyncStartedOnCmHandles.put('ch-1', 'started') + when: 'remove cm handle entry' + objectUnderTest.removeResetCmHandleFromModuleSyncMap('ch-1') + then: 'an event is logged with level INFO' + def loggingEvent = getLoggingEvent() + assert loggingEvent.level == Level.INFO + and: 'the log indicates the cm handle entry is removed successfully' + assert loggingEvent.formattedMessage == 'ch-1 removed from in progress map' + } + + def 'Remove non-existing cm handle id from hazelcast map'() { + given: 'hazelcast map does not contains cm handle id' + def result = moduleSyncStartedOnCmHandles.get('non-existing-cm-handle') + assert result == null + when: 'remove cm handle entry from hazelcast map' + objectUnderTest.removeResetCmHandleFromModuleSyncMap('non-existing-cm-handle') + then: 'no event is logged' + def loggingEvent = getLoggingEvent() + assert loggingEvent == null + } + + def advisedCmHandleAsDataNode(cmHandleId) { + return new DataNode(anchorName: cmHandleId, leaves: ['id': cmHandleId, 'cm-handle-state': 'ADVISED']) + } + + def assertYamgModelCmHandleArgument(args, expectedCmHandleId) { + { + def yangModelCmHandle = args[0] + assert yangModelCmHandle.id == expectedCmHandleId + } + return true + } + + def assertBatch(args, expectedCmHandleStatePerCmHandleIds, expectedCmHandleState) { + { + Map actualCmHandleStatePerCmHandle = args[0] + assert actualCmHandleStatePerCmHandle.size() == expectedCmHandleStatePerCmHandleIds.size() + actualCmHandleStatePerCmHandle.each { + assert expectedCmHandleStatePerCmHandleIds.contains(it.key.id) + assert it.value == expectedCmHandleState + } + } + return true + } + + def resetModuleSyncStartedOnCmHandles(moduleSyncStartedOnCmHandles) { + moduleSyncStartedOnCmHandles.clear(); + } + + def getLoggingEvent() { + return logger.list[0] + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncWatchdogSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncWatchdogSpec.groovy new file mode 100644 index 0000000000..7425a52ff1 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncWatchdogSpec.groovy @@ -0,0 +1,124 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2023 Nordix Foundation + * 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. + * 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.impl.inventory.sync + +import com.hazelcast.map.IMap +import org.onap.cps.ncmp.api.impl.inventory.sync.ModuleSyncTasks +import org.onap.cps.ncmp.api.impl.inventory.sync.ModuleSyncWatchdog +import org.onap.cps.ncmp.api.impl.inventory.sync.SyncUtils +import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle +import org.onap.cps.ncmp.api.impl.inventory.sync.executor.AsyncTaskExecutor +import java.util.concurrent.ArrayBlockingQueue +import org.onap.cps.spi.model.DataNode +import spock.lang.Specification + +class ModuleSyncWatchdogSpec extends Specification { + + def mockSyncUtils = Mock(SyncUtils) + + def static testQueueCapacity = 50 + 2 * ModuleSyncWatchdog.MODULE_SYNC_BATCH_SIZE + + def moduleSyncWorkQueue = new ArrayBlockingQueue(testQueueCapacity) + + def mockModuleSyncStartedOnCmHandles = Mock(IMap) + + def mockModuleSyncTasks = Mock(ModuleSyncTasks) + + def spiedAsyncTaskExecutor = Spy(AsyncTaskExecutor) + + def objectUnderTest = new ModuleSyncWatchdog(mockSyncUtils, moduleSyncWorkQueue , mockModuleSyncStartedOnCmHandles, mockModuleSyncTasks, spiedAsyncTaskExecutor) + + void setup() { + spiedAsyncTaskExecutor.setupThreadPool() + } + + def 'Module sync advised cm handles with #scenario.'() { + given: 'sync utilities returns #numberOfAdvisedCmHandles advised cm handles' + mockSyncUtils.getAdvisedCmHandles() >> createDataNodes(numberOfAdvisedCmHandles) + and: 'the executor has enough available threads' + spiedAsyncTaskExecutor.getAsyncTaskParallelismLevel() >> 3 + when: ' module sync is started' + objectUnderTest.moduleSyncAdvisedCmHandles() + then: 'it performs #expectedNumberOfTaskExecutions tasks' + expectedNumberOfTaskExecutions * spiedAsyncTaskExecutor.executeTask(*_) + where: 'the following parameter are used' + scenario | numberOfAdvisedCmHandles || expectedNumberOfTaskExecutions + 'less then 1 batch' | 1 || 1 + 'exactly 1 batch' | ModuleSyncWatchdog.MODULE_SYNC_BATCH_SIZE || 1 + '2 batches' | 2 * ModuleSyncWatchdog.MODULE_SYNC_BATCH_SIZE || 2 + 'queue capacity' | testQueueCapacity || 3 + 'over queue capacity' | testQueueCapacity + 2 * ModuleSyncWatchdog.MODULE_SYNC_BATCH_SIZE || 3 + } + + def 'Module sync advised cm handles starts with no available threads.'() { + given: 'sync utilities returns a advise cm handles' + mockSyncUtils.getAdvisedCmHandles() >> createDataNodes(1) + and: 'the executor first has no threads but has one thread on the second attempt' + spiedAsyncTaskExecutor.getAsyncTaskParallelismLevel() >>> [ 0, 1 ] + when: ' module sync is started' + objectUnderTest.moduleSyncAdvisedCmHandles() + then: 'it performs one task' + 1 * spiedAsyncTaskExecutor.executeTask(*_) + } + + def 'Module sync advised cm handles already handled.'() { + given: 'sync utilities returns a advise cm handles' + mockSyncUtils.getAdvisedCmHandles() >> createDataNodes(1) + and: 'the executor has a thread available' + spiedAsyncTaskExecutor.getAsyncTaskParallelismLevel() >> 1 + and: 'the semaphore cache indicates the cm handle is already being processed' + mockModuleSyncStartedOnCmHandles.putIfAbsent(*_) >> 'Started' + when: ' module sync is started' + objectUnderTest.moduleSyncAdvisedCmHandles() + then: 'it does NOT execute a task to process the (empty) batch' + 0 * spiedAsyncTaskExecutor.executeTask(*_) + } + + def 'Module sync with previous cm handle(s) left in work queue.'() { + given: 'there is still a cm handle in the queue' + moduleSyncWorkQueue.offer(new DataNode()) + and: 'sync utilities returns many advise cm handles' + mockSyncUtils.getAdvisedCmHandles() >> createDataNodes(500) + and: 'the executor has plenty threads available' + spiedAsyncTaskExecutor.getAsyncTaskParallelismLevel() >> 10 + when: ' module sync is started' + objectUnderTest.moduleSyncAdvisedCmHandles() + then: 'it does executes only one task to process the remaining handle in the queue' + 1 * spiedAsyncTaskExecutor.executeTask(*_) + } + + def 'Reset failed cm handles.'() { + given: 'sync utilities returns failed cm handles' + def failedCmHandles = [new YangModelCmHandle()] + mockSyncUtils.getCmHandlesThatFailedModelSyncOrUpgrade() >> failedCmHandles + when: 'reset failed cm handles is started' + objectUnderTest.resetPreviouslyFailedCmHandles() + then: 'it is delegated to the module sync task (service)' + 1 * mockModuleSyncTasks.resetFailedCmHandles(failedCmHandles) + } + + def createDataNodes(numberOfDataNodes) { + def dataNodes = [] + (1..numberOfDataNodes).each {dataNodes.add(new DataNode())} + return dataNodes + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/SyncUtilsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/SyncUtilsSpec.groovy new file mode 100644 index 0000000000..025d9482d6 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/SyncUtilsSpec.groovy @@ -0,0 +1,198 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2023 Nordix Foundation + * 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. + * 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.impl.inventory.sync + +import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.LOCKED_MISBEHAVING +import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL +import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_SYNC_FAILED +import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_UPGRADE +import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_UPGRADE_FAILED + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.core.read.ListAppender +import org.onap.cps.ncmp.api.impl.inventory.sync.SyncUtils +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.AnnotationConfigApplicationContext +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.inventory.CmHandleQueries +import org.onap.cps.ncmp.api.impl.inventory.CmHandleState +import org.onap.cps.ncmp.api.impl.inventory.CompositeState +import org.onap.cps.ncmp.api.impl.inventory.CompositeStateBuilder +import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState +import org.onap.cps.spi.FetchDescendantsOption +import org.onap.cps.spi.model.DataNode +import org.onap.cps.utils.JsonObjectMapper +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import spock.lang.Specification +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter +import java.util.stream.Collectors + +class SyncUtilsSpec extends Specification{ + + def mockCmHandleQueries = Mock(CmHandleQueries) + + def mockDmiDataOperations = Mock(DmiDataOperations) + + def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) + + def objectUnderTest = new SyncUtils(mockCmHandleQueries, mockDmiDataOperations, jsonObjectMapper) + + def static neverUpdatedBefore = '1900-01-01T00:00:00.000+0100' + + def static now = OffsetDateTime.now() + + def static nowAsString = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(now) + + def static dataNode = new DataNode(leaves: ['id': 'cm-handle-123']) + + def applicationContext = new AnnotationConfigApplicationContext() + + def logger = (Logger) LoggerFactory.getLogger(SyncUtils) + def loggingListAppender + + void setup() { + logger.setLevel(Level.DEBUG) + loggingListAppender = new ListAppender() + logger.addAppender(loggingListAppender) + loggingListAppender.start() + applicationContext.refresh() + } + + void cleanup() { + ((Logger) LoggerFactory.getLogger(SyncUtils.class)).detachAndStopAllAppenders() + applicationContext.close() + } + + def 'Get an advised Cm-Handle where ADVISED cm handle #scenario'() { + given: 'the inventory persistence service returns a collection of data nodes' + mockCmHandleQueries.queryCmHandlesByState(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 + where: 'the following scenarios are used' + scenario | dataNodeCollection || expectedCallsToGetYangModelCmHandle | expectedDataNodeSize + 'exists' | [dataNode] || 1 | 1 + 'does not exist' | [] || 0 | 0 + } + + def 'Update Lock Reason, Details and Attempts where lock reason #scenario'() { + given: 'A locked state' + def compositeState = new CompositeState(lockReason: lockReason) + when: 'update cm handle details and attempts is called' + objectUnderTest.updateLockReasonDetailsAndAttempts(compositeState, MODULE_SYNC_FAILED, 'new error message') + then: 'the composite state lock reason and details are updated' + assert compositeState.lockReason.lockReasonCategory == MODULE_SYNC_FAILED + assert compositeState.lockReason.details == expectedDetails + where: + scenario | lockReason || expectedDetails + 'does not exist' | null || 'Attempt #1 failed: new error message' + 'exists' | CompositeState.LockReason.builder().details("Attempt #2 failed: some error message").build() || 'Attempt #3 failed: new error message' + } + + def 'Get all locked Cm-Handle where Lock Reason is MODULE_SYNC_FAILED cm handle #scenario'() { + given: 'the cps (persistence service) returns a collection of data nodes' + mockCmHandleQueries.queryCmHandleAncestorsByCpsPath( + '//lock-reason[@reason="MODULE_SYNC_FAILED" or @reason="MODULE_UPGRADE"]', + FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode] + when: 'get locked Misbehaving cm handle is called' + def result = objectUnderTest.getCmHandlesThatFailedModelSyncOrUpgrade() + then: 'the returned cm handle collection is the correct size' + result.size() == 1 + and: 'the correct cm handle is returned' + result[0].id == 'cm-handle-123' + } + + def 'Retry Locked Cm-Handle where the last update time is #scenario'() { + given: 'Last update was #lastUpdateMinutesAgo minutes ago (-1 means never)' + def lastUpdatedTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(now.minusMinutes(lastUpdateMinutesAgo)) + if (lastUpdateMinutesAgo < 0 ) { + lastUpdatedTime = neverUpdatedBefore + } + when: 'checking to see if cm handle is ready for retry' + def result = objectUnderTest.needsModuleSyncRetryOrUpgrade(new CompositeStateBuilder() + .withLockReason(MODULE_SYNC_FAILED, lockDetails) + .withLastUpdatedTime(lastUpdatedTime).build()) + then: 'retry is only attempted when expected' + assert result == retryExpected + and: 'logs contain related information' + def logs = loggingListAppender.list.toString() + assert logs.contains(logReason) + where: 'the following parameters are used' + scenario | lastUpdateMinutesAgo | lockDetails | logReason || retryExpected + 'never attempted before' | -1 | 'fist attempt:' | 'First Attempt:' || true + '1st attempt, last attempt > 2 minute ago' | 3 | 'Attempt #1 failed:' | 'Retry due now' || true + '2nd attempt, last attempt < 4 minutes ago' | 1 | 'Attempt #2 failed:' | 'Time until next attempt is 3 minutes:' || false + '2nd attempt, last attempt > 4 minutes ago' | 5 | 'Attempt #2 failed:' | 'Retry due now' || true + } + + def 'Retry Locked Cm-Handle with other lock reasons (category) #lockReasonCategory'() { + when: 'checking to see if cm handle is ready for retry' + def result = objectUnderTest.needsModuleSyncRetryOrUpgrade(new CompositeStateBuilder() + .withLockReason(lockReasonCategory, 'some details') + .withLastUpdatedTime(nowAsString).build()) + then: 'verify retry attempts' + assert result == retryAttempt + and: 'logs contain related information' + def logs = loggingListAppender.list.toString() + assert logs.contains(logReason) + where: 'the following lock reasons occurred' + scenario | lockReasonCategory || logReason | retryAttempt + 'module upgrade' | MODULE_UPGRADE || 'Locked for module upgrade.' | true + 'module sync failed' | MODULE_SYNC_FAILED || 'First Attempt:' | false + 'lock misbehaving' | LOCKED_MISBEHAVING || 'Locked for other reason' | false + } + + def 'Get a Cm-Handle where #scenario'() { + given: 'the inventory persistence service returns a collection of data nodes' + mockCmHandleQueries.queryCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) >> unSynchronizedDataNodes + mockCmHandleQueries.cmHandleHasState('cm-handle-123', CmHandleState.READY) >> cmHandleHasState + when: 'get advised cm handles are fetched' + def yangModelCollection = objectUnderTest.getUnsynchronizedReadyCmHandles() + then: 'the returned data node collection is the correct size' + yangModelCollection.size() == expectedDataNodeSize + and: 'the result contains the correct data' + yangModelCollection.stream().map(yangModel -> yangModel.id).collect(Collectors.toSet()) == expectedYangModelCollectionIds + where: 'the following scenarios are used' + scenario | unSynchronizedDataNodes | cmHandleHasState || expectedDataNodeSize | expectedYangModelCollectionIds + 'a Cm-Handle unsynchronized and ready' | [dataNode] | true || 1 | ['cm-handle-123'] as Set + 'a Cm-Handle unsynchronized but not ready' | [dataNode] | false || 0 | [] as Set + 'all Cm-Handle synchronized' | [] | false || 0 | [] as Set + } + + def 'Get resource data through DMI Operations #scenario'() { + given: 'the inventory persistence service returns a collection of data nodes' + def jsonString = '{"stores:bookstore":{"categories":[{"code":"01"}]}}' + JsonNode jsonNode = jsonObjectMapper.convertToJsonNode(jsonString); + def responseEntity = new ResponseEntity<>(jsonNode, HttpStatus.OK) + mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_OPERATIONAL.datastoreName, 'cm-handle-123', _) >> responseEntity + when: 'get resource data is called' + def result = objectUnderTest.getResourceData('cm-handle-123') + then: 'the returned data is correct' + result == jsonString + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/config/WatchdogSchedulingConfigurerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/config/WatchdogSchedulingConfigurerSpec.groovy new file mode 100644 index 0000000000..7760b39cb3 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/config/WatchdogSchedulingConfigurerSpec.groovy @@ -0,0 +1,59 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2023 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.impl.inventory.sync.config + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.onap.cps.ncmp.api.impl.inventory.sync.config.WatchdogSchedulingConfigurer +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +@SpringBootTest +@ContextConfiguration(classes = [ConfigurableApplicationContext, WatchdogSchedulingConfigurer]) +class WatchdogSchedulingConfigurerSpec extends Specification { + + @Autowired + private ConfigurableApplicationContext applicationContext; + + def watchdogSchedulingConfigurer; + + @BeforeEach + void setup() { + watchdogSchedulingConfigurer = (WatchdogSchedulingConfigurer) applicationContext.getBean("watchdogSchedulingConfigurer") + } + + @AfterEach + void tearDown() { + if (applicationContext != null) { + applicationContext.close() + } + } + + def 'Validate watchdog scheduling configuration'() { + given: 'task scheduler configuration properties are loaded as map' + def linkedHashMap = watchdogSchedulingConfigurer.taskScheduler().getProperties() + expect: 'thread name prefix is mapped correctly' + assert linkedHashMap.'threadNamePrefix' == 'watchdog-th-' + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/executor/AsyncTaskExecutorSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/executor/AsyncTaskExecutorSpec.groovy new file mode 100644 index 0000000000..64ecafefc7 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/sync/executor/AsyncTaskExecutorSpec.groovy @@ -0,0 +1,62 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2023 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.impl.inventory.sync.executor + +import org.onap.cps.ncmp.api.impl.inventory.sync.executor.AsyncTaskExecutor +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import spock.lang.Specification +import java.util.concurrent.TimeoutException +import java.util.function.Supplier + +@SpringBootTest(classes = AsyncTaskExecutor) +class AsyncTaskExecutorSpec extends Specification { + + @Autowired + AsyncTaskExecutor objectUnderTest + def mockTaskSupplier = Mock(Supplier) + + def 'Parallelism level configuration.'() { + expect: 'Parallelism level is configured with the correct value' + assert objectUnderTest.getAsyncTaskParallelismLevel() == 3 + } + + def 'Task completion with #caseDescriptor.'() { + when: 'task completion is handled' + def irrelevantResponse = null + objectUnderTest.handleTaskCompletion(irrelevantResponse, exception); + then: 'any exception is swallowed by the task completion (logged)' + noExceptionThrown() + where: 'following cases are tested' + caseDescriptor | exception + 'no exception' | null + 'time out exception' | new TimeoutException("time-out") + 'unexpected exception' | new Exception("some exception") + } + + def 'Task execution.'() { + when: 'a task is submitted for execution' + objectUnderTest.executeTask(() -> mockTaskSupplier, 0) + then: 'the task submission is successful' + noExceptionThrown() + } + +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImplSpec.groovy deleted file mode 100644 index a3a5efc748..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImplSpec.groovy +++ /dev/null @@ -1,201 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 Nordix Foundation - * Modifications Copyright (C) 2023 TechMahindra Ltd. - * ================================================================================ - * 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.impl.trustlevel.TrustLevel - -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.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS -import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS - -import com.hazelcast.map.IMap -import org.onap.cps.ncmp.api.impl.inventory.CmHandleQueriesImpl -import org.onap.cps.ncmp.api.impl.inventory.CmHandleState -import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState -import org.onap.cps.spi.CpsDataPersistenceService -import org.onap.cps.spi.model.DataNode -import spock.lang.Shared -import spock.lang.Specification - -class CmHandleQueriesImplSpec extends Specification { - def cpsDataPersistenceService = Mock(CpsDataPersistenceService) - def trustLevelPerCmHandle = [ 'my completed cm handle': TrustLevel.COMPLETE, 'my untrusted cm handle': TrustLevel.NONE ] - - def objectUnderTest = new CmHandleQueriesImpl(cpsDataPersistenceService, trustLevelPerCmHandle) - - @Shared - def static sampleDataNodes = [new DataNode()] - - def dataNodeWithPrivateField = '//additional-properties[@name=\"Contact3\" and @value=\"newemailforstore3@bookstore.com\"]/ancestor::cm-handles' - - def static pnfDemo = createDataNode('PNFDemo') - def static pnfDemo2 = createDataNode('PNFDemo2') - def static pnfDemo3 = createDataNode('PNFDemo3') - def static pnfDemo4 = createDataNode('PNFDemo4') - def static pnfDemo5 = createDataNode('PNFDemo5') - - 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 result = objectUnderTest.queryCmHandlePublicProperties(publicPropertyPairs) - then: 'the correct cm handle data objects are returned' - result.containsAll(expectedCmHandleIds) - result.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 cm handles on trust level'() { - given: 'query properties for trustlevel COMPLETE' - def trustLevelPropertyQueryPairs = ['trustLevel' : TrustLevel.COMPLETE.toString()] - when: 'the query is executed' - def result = objectUnderTest.queryCmHandlesByTrustLevel(trustLevelPropertyQueryPairs) - then: 'the result only contains the completed cm handle' - assert result.size() == 1 - assert result[0] == 'my completed cm handle' - } - - def 'Query CmHandles using empty public properties query pair.'() { - when: 'a query on CmHandle public properties is executed using an empty map' - def result = objectUnderTest.queryCmHandlePublicProperties([:]) - then: 'no cm handles are returned' - result.size() == 0 - } - - def 'Query CmHandles using empty private properties query pair.'() { - when: 'a query on CmHandle private properties is executed using an empty map' - def result = objectUnderTest.queryCmHandleAdditionalProperties([:]) - then: 'no cm handles are returned' - result.size() == 0 - } - - def 'Query CmHandles by a private field\'s value.'() { - given: 'a data node exists with a certain additional-property' - cpsDataPersistenceService.queryDataNodes(_, _, dataNodeWithPrivateField, _) >> [pnfDemo5] - when: 'a query on CmHandle private properties is executed using a map' - def result = objectUnderTest.queryCmHandleAdditionalProperties(['Contact3': 'newemailforstore3@bookstore.com']) - then: 'one cm handle is returned' - result.size() == 1 - } - - def 'Get CmHandles by it\'s 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_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - '//state[@cm-handle-state="ADVISED"]/ancestor::cm-handles', INCLUDE_ALL_DESCENDANTS) >> sampleDataNodes - when: 'cm handles are fetched by state' - def result = objectUnderTest.queryCmHandlesByState(cmHandleState) - then: 'the returned result matches the result from the persistence service' - assert result == sampleDataNodes - } - - def 'Check the state of a cmHandle when #scenario.'() { - given: 'a cm handle state to compare' - def cmHandleState = state - and: 'the persistence service returns a list of data nodes' - cpsDataPersistenceService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - NCMP_DMI_REGISTRY_PARENT + '/cm-handles[@id=\'some-cm-handle\']/state', - OMIT_DESCENDANTS) >> [new DataNode(leaves: ['cm-handle-state': 'READY'])] - when: 'cm handles are compared by state' - def result = objectUnderTest.cmHandleHasState('some-cm-handle', cmHandleState) - then: 'the returned result matches the expected result from the persistence service' - result == expectedResult - where: - scenario | state || expectedResult - 'the provided state matches' | CmHandleState.READY || true - 'the provided state does not match'| CmHandleState.DELETED || false - } - - def 'Get Cm Handles state by 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.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - NCMP_DMI_REGISTRY_PARENT + '/cm-handles[@id=\'some-cm-handle\']/state', - OMIT_DESCENDANTS) >> [new DataNode(leaves: ['cm-handle-state': 'READY'])] - when: 'cm handles are fetched by state and id' - def result = objectUnderTest.getCmHandleState('some-cm-handle') - then: 'the returned result is a list of data nodes returned by cps data service' - assert result == new DataNode(leaves: ['cm-handle-state': 'READY']) - } - - def 'Retrieve 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_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - '//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.queryCmHandlesByOperationalSyncState(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_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - cpsPath + '/ancestor::cm-handles', INCLUDE_ALL_DESCENDANTS) - >> Arrays.asList(cmHandleDataNode) - when: 'get cm handles by cps path is invoked' - def result = objectUnderTest.queryCmHandleAncestorsByCpsPath(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 all cm handles by dmi plugin identifier'() { - given: 'the DataNodes queried for a given cpsPath are returned from the persistence service.' - mockResponses() - when: 'cm Handles are fetched for a given dmi plugin identifier' - def result = objectUnderTest.getCmHandleIdsByDmiPluginIdentifier('my-dmi-plugin-identifier') - then: 'result is the correct size' - assert result.size() == 3 - and: 'result contains the correct cm handles' - assert result.containsAll('PNFDemo', 'PNFDemo2', 'PNFDemo4') - } - - 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] - cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@dmi-service-name=\'my-dmi-plugin-identifier\']', OMIT_DESCENDANTS) >> [pnfDemo, pnfDemo2] - cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@dmi-data-service-name=\'my-dmi-plugin-identifier\']', OMIT_DESCENDANTS) >> [pnfDemo, pnfDemo4] - cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@dmi-model-service-name=\'my-dmi-plugin-identifier\']', OMIT_DESCENDANTS) >> [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/CompositeStateBuilderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CompositeStateBuilderSpec.groovy deleted file mode 100644 index c2cdd9bc2e..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CompositeStateBuilderSpec.groovy +++ /dev/null @@ -1,92 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022 Bell Canada - * Modifications Copyright (C) 2022-2023 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.impl.inventory.CmHandleState -import org.onap.cps.ncmp.api.impl.inventory.CompositeState -import org.onap.cps.ncmp.api.impl.inventory.CompositeStateBuilder -import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState -import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory -import org.onap.cps.spi.model.DataNode -import org.onap.cps.spi.model.DataNodeBuilder -import spock.lang.Specification - -import java.time.OffsetDateTime -import java.time.ZoneOffset -import java.time.format.DateTimeFormatter - -class CompositeStateBuilderSpec extends Specification { - - def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ") - .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC)) - - def static cmHandleId = 'myHandle1' - def static cmHandleXpath = "/dmi-registry/cm-handles[@id='${cmHandleId}/state']" - def static stateDataNodes = [new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/state/lock-reason") - .withLeaves(['reason': 'MODULE_SYNC_FAILED', 'details': 'lock details']).build(), - new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/state/datastores") - .withChildDataNodes(Arrays.asList(new DataNodeBuilder() - .withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/state/datastores/operational") - .withLeaves(['sync-state': 'UNSYNCHRONIZED']).build())).build()] - def static cmHandleDataNode = new DataNode(xpath: cmHandleXpath, childDataNodes: stateDataNodes, leaves: ['cm-handle-state': 'ADVISED']) - - def "Composite State Specification"() { - when: 'using composite state builder ' - def compositeState = new CompositeStateBuilder().withCmHandleState(CmHandleState.ADVISED) - .withLockReason(LockReasonCategory.MODULE_SYNC_FAILED,"").withOperationalDataStores(DataStoreSyncState.UNSYNCHRONIZED, - formattedDateAndTime.toString()).withLastUpdatedTime(formattedDateAndTime).build() - then: 'it matches expected cm handle state and data store sync state' - assert compositeState.cmHandleState == CmHandleState.ADVISED - assert compositeState.dataStores.operationalDataStore.dataStoreSyncState == DataStoreSyncState.UNSYNCHRONIZED - } - - def "Build composite state from DataNode "() { - given: "a Data Node " - new DataNode(leaves: ['cm-handle-state': 'ADVISED']) - when: 'build from data node function is invoked' - def compositeState = new CompositeStateBuilder().fromDataNode(cmHandleDataNode).build() - then: 'it matches expected state model as JSON' - assert compositeState.cmHandleState == CmHandleState.ADVISED - } - - def 'CompositeStateBuilder build'() { - given: 'A CompositeStateBuilder with all private fields set' - def finalCompositeStateBuilder = new CompositeStateBuilder() - .withCmHandleState(CmHandleState.ADVISED) - .withLastUpdatedTime(formattedDateAndTime.toString()) - .withLockReason(LockReasonCategory.MODULE_SYNC_FAILED, 'locked details') - .withOperationalDataStores(DataStoreSyncState.SYNCHRONIZED, formattedDateAndTime) - when: 'build is called' - def result = finalCompositeStateBuilder.build() - then: 'result is of the correct type' - assert result.class == CompositeState.class - and: 'built result should have correct values' - assert !result.getDataSyncEnabled() - assert result.getLastUpdateTime() == formattedDateAndTime - assert result.getLockReason().getLockReasonCategory() == LockReasonCategory.MODULE_SYNC_FAILED - assert result.getLockReason().getDetails() == 'locked details' - assert result.getCmHandleState() == CmHandleState.ADVISED - assert result.getDataStores().getOperationalDataStore().getDataStoreSyncState() == DataStoreSyncState.SYNCHRONIZED - assert result.getDataStores().getOperationalDataStore().getLastSyncTime() == formattedDateAndTime - } - -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CompositeStateSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CompositeStateSpec.groovy deleted file mode 100644 index 985bdc5cd4..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CompositeStateSpec.groovy +++ /dev/null @@ -1,64 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 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 com.fasterxml.jackson.databind.ObjectMapper -import org.onap.cps.ncmp.api.impl.inventory.CmHandleState -import org.onap.cps.ncmp.api.impl.inventory.CompositeState -import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState -import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory -import spock.lang.Specification -import java.time.OffsetDateTime -import java.time.ZoneOffset -import java.time.format.DateTimeFormatter - -import static org.onap.cps.ncmp.api.impl.inventory.CompositeState.DataStores -import static org.onap.cps.ncmp.api.impl.inventory.CompositeState.Operational -import static org.onap.cps.ncmp.utils.TestUtils.getResourceFileContent -import static org.springframework.util.StringUtils.trimAllWhitespace - -class CompositeStateSpec extends Specification { - - def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ") - .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC)) - def objectMapper = new ObjectMapper() - - def "Composite State Specification"() { - given: "a Composite State" - def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, - lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.MODULE_SYNC_FAILED).details("lock details").build(), - lastUpdateTime: formattedDateAndTime.toString(), - dataSyncEnabled: false, - dataStores: dataStores()) - when: 'it is represented as JSON' - def resultJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(compositeState) - then: 'it matches expected state model as JSON' - def expectedJson = getResourceFileContent('expectedStateModel.json') - assert trimAllWhitespace(expectedJson) == trimAllWhitespace(resultJson) - } - - def dataStores() { - DataStores.builder().operationalDataStore(Operational.builder() - .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED) - .lastSyncTime(formattedDateAndTime.toString()).build()) - .build() - } -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy deleted file mode 100644 index 724f05b1b8..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy +++ /dev/null @@ -1,311 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 Nordix Foundation - * Modifications Copyright (C) 2022 Bell Canada - * Modifications Copyright (C) 2023 TechMahindra Ltd. - * ================================================================================ - * 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.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME -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.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NO_TIMESTAMP -import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS - -import com.fasterxml.jackson.databind.ObjectMapper -import org.onap.cps.api.CpsAdminService -import org.onap.cps.api.CpsDataService -import org.onap.cps.api.CpsModuleService -import org.onap.cps.ncmp.api.impl.inventory.CmHandleState -import org.onap.cps.ncmp.api.impl.inventory.CompositeState -import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistenceImpl -import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle -import org.onap.cps.spi.CascadeDeleteAllowed -import org.onap.cps.spi.FetchDescendantsOption -import org.onap.cps.spi.model.DataNode -import org.onap.cps.spi.model.ModuleDefinition -import org.onap.cps.spi.model.ModuleReference -import org.onap.cps.utils.JsonObjectMapper -import org.onap.cps.spi.utils.CpsValidator -import spock.lang.Shared -import spock.lang.Specification -import java.time.OffsetDateTime -import java.time.ZoneOffset -import java.time.format.DateTimeFormatter - -class InventoryPersistenceImplSpec extends Specification { - - def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper())) - - def mockCpsDataService = Mock(CpsDataService) - - def mockCpsModuleService = Mock(CpsModuleService) - - def mockCpsAdminService = Mock(CpsAdminService) - - def mockCpsValidator = Mock(CpsValidator) - - def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService, - mockCpsValidator, mockCpsAdminService) - - def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ") - .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC)) - - def cmHandleId = 'some-cm-handle' - def leaves = ["dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"] - def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']" - - def cmHandleId2 = 'another-cm-handle' - def xpath2 = "/dmi-registry/cm-handles[@id='another-cm-handle']" - - @Shared - def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]), - new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])] - - @Shared - def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])] - - @Shared - def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])] - - @Shared - def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])] - - 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) - mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode] - when: 'retrieving the yang modelled cm handle' - def result = objectUnderTest.getYangModelCmHandle(cmHandleId) - then: 'the result has the correct id and service names' - result.id == cmHandleId - result.dmiServiceName == 'common service name' - result.dmiDataServiceName == 'data service name' - result.dmiModelServiceName == 'model service name' - and: 'the expected DMI properties' - result.dmiProperties == expectedDmiProperties - result.publicProperties == expectedPublicProperties - and: 'the state details are returned' - result.compositeState.cmHandleState == expectedCompositeState - and: 'the CM Handle ID is validated' - 1 * mockCpsValidator.validateNameCharacters(cmHandleId) - where: 'the following parameters are used' - scenario | childDataNodes || expectedDmiProperties || expectedPublicProperties || expectedCompositeState - 'no properties' | [] || [] || [] || null - 'DMI and public properties' | childDataNodesForCmHandleWithAllProperties || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")] || null - 'just DMI properties' | childDataNodesForCmHandleWithDMIProperties || [new YangModelCmHandle.Property("name1", "value1")] || [] || null - 'just public properties' | childDataNodesForCmHandleWithPublicProperties || [] || [new YangModelCmHandle.Property("name2", "value2")] || null - 'with state details' | childDataNodesForCmHandleWithState || [] || [] || CmHandleState.ADVISED - } - - def "Handling missing service names as null."() { - given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes' - def dataNode = new DataNode(childDataNodes:[], leaves: [:]) - mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode] - when: 'retrieving the yang modelled cm handle' - def result = objectUnderTest.getYangModelCmHandle(cmHandleId) - then: 'the service names are returned as null' - result.dmiServiceName == null - result.dmiDataServiceName == null - result.dmiModelServiceName == null - and: 'the CM Handle ID is validated' - 1 * mockCpsValidator.validateNameCharacters(cmHandleId) - } - - def "Retrieve multiple YangModelCmHandles"() { - given: 'the cps data service returns 2 data nodes from the DMI registry' - def dataNodes = [new DataNode(xpath: xpath), new DataNode(xpath: xpath2)] - mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes - when: 'retrieving the yang modelled cm handle' - def results = objectUnderTest.getYangModelCmHandles([cmHandleId, cmHandleId2]) - then: 'verify both have returned and cmhandleIds are correct' - assert results.size() == 2 - assert results.id.containsAll([cmHandleId, cmHandleId2]) - } - - def 'Get a Cm Handle Composite State'() { - given: 'a valid cm handle id' - def cmHandleId = 'Some-Cm-Handle' - def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED']) - and: 'cps data service returns a valid data node' - mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode] - when: 'get cm handle state is invoked' - def result = objectUnderTest.getCmHandleState(cmHandleId) - then: 'result has returned the correct cm handle state' - result.cmHandleState == CmHandleState.ADVISED - and: 'the CM Handle ID is validated' - 1 * mockCpsValidator.validateNameCharacters(cmHandleId) - } - - def 'Update Cm Handle with #scenario State'() { - given: 'a cm handle and a composite state' - def cmHandleId = 'Some-Cm-Handle' - def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime) - when: 'update cm handle state is invoked with the #scenario state' - objectUnderTest.saveCmHandleState(cmHandleId, compositeState) - then: 'update node leaves is invoked with the correct params' - 1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime) - where: 'the following states are used' - scenario | cmHandleState || expectedJsonData - 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}' - 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}' - 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}' - } - - def 'Update Cm Handles with #scenario States'() { - given: 'a map of cm handles composite states' - def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime) - def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime) - when: 'update cm handle state is invoked with the #scenario state' - def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2] - objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap) - then: 'update node leaves is invoked with the correct params' - 1 * mockCpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandlesJsonDataMap, _ as OffsetDateTime) - where: 'the following states are used' - scenario | cmHandleState || cmHandlesJsonDataMap - 'READY' | CmHandleState.READY || ['/dmi-registry/cm-handles[@id=\'Some-Cm-Handle1\']':'{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle2\']':'{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'] - 'LOCKED' | CmHandleState.LOCKED || ['/dmi-registry/cm-handles[@id=\'Some-Cm-Handle1\']':'{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle2\']':'{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'] - 'DELETING' | CmHandleState.DELETING || ['/dmi-registry/cm-handles[@id=\'Some-Cm-Handle1\']':'{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle2\']':'{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'] - } - - def 'Get module definitions'() { - given: 'cps module service returns a collection of module definitions' - def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')] - mockCpsModuleService.getModuleDefinitionsByAnchorName(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleDefinitions - when: 'get module definitions by cmHandle is invoked' - def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id') - then: 'the returned result are the same module definitions as returned from the module service' - assert result == moduleDefinitions - } - - def 'Get module references'() { - given: 'cps module service returns a collection of module references' - def moduleReferences = [new ModuleReference('moduleName','revision','namespace')] - mockCpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleReferences - when: 'get yang resources module references by cmHandle is invoked' - def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id') - then: 'the returned result is a collection of module definitions' - assert result == moduleReferences - and: 'the CM Handle ID is validated' - 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id') - } - - def 'Save Cmhandle'() { - given: 'cmHandle represented as Yang Model' - def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: []) - when: 'the method to save cmhandle is called' - objectUnderTest.saveCmHandle(yangModelCmHandle) - then: 'the data service method to save list elements is called once' - 1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT, - _,null) >> { - args -> { - assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}') - } - } - } - - def 'Save Multiple Cmhandles'() { - given: 'cm handles represented as Yang Model' - def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1') - def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2') - when: 'the cm handles are saved' - objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2]) - then: 'CPS Data Service persists both cm handles as a batch' - 1 * mockCpsDataService.saveListElementsBatch(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - NCMP_DMI_REGISTRY_PARENT, _,null) >> { - args -> { - def jsonDataList = (args[3] as List) - (jsonDataList[0] as String).contains('cmhandle1') - (jsonDataList[0] as String).contains('cmhandle2') - } - } - } - - def 'Delete list or list elements'() { - when: 'the method to delete list or list elements is called' - objectUnderTest.deleteListOrListElement('sample xPath') - then: 'the data service method to save list elements is called once' - 1 * mockCpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath',null) - } - - def 'Delete schema set with a valid schema set name'() { - when: 'the method to delete schema set is called with valid schema set name' - objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName') - then: 'the module service to delete schemaSet is invoked once' - 1 * mockCpsModuleService.deleteSchemaSet(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED) - and: 'the schema set name is validated' - 1 * mockCpsValidator.validateNameCharacters('validSchemaSetName') - } - - def 'Delete multiple schema sets with valid schema set names'() { - when: 'the method to delete schema sets is called with valid schema set names' - objectUnderTest.deleteSchemaSetsWithCascade(['validSchemaSetName1', 'validSchemaSetName2']) - then: 'the module service to delete schema sets is invoked once' - 1 * mockCpsModuleService.deleteSchemaSetsWithCascade(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['validSchemaSetName1', 'validSchemaSetName2']) - and: 'the schema set names are validated' - 1 * mockCpsValidator.validateNameCharacters(['validSchemaSetName1', 'validSchemaSetName2']) - } - - def 'Get data node via xPath'() { - when: 'the method to get data nodes is called' - objectUnderTest.getDataNode('sample xPath') - then: 'the data persistence service method to get data node is invoked once' - 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath', INCLUDE_ALL_DESCENDANTS) - } - - def 'Get cmHandle data node'() { - given: 'expected xPath to get cmHandle data node' - def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']'; - when: 'the method to get data nodes is called' - objectUnderTest.getCmHandleDataNode('sample cmHandleId') - then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath' - 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, expectedXPath, INCLUDE_ALL_DESCENDANTS) - } - - def 'Get CM handles that has given module names'() { - when: 'the method to get cm handles is called' - objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name']) - then: 'the admin persistence service method to query anchors is invoked once with the same parameter' - 1 * mockCpsAdminService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name']) - } - - def 'Replace list content'() { - when: 'replace list content method is called with xpath and data nodes collection' - objectUnderTest.replaceListContent('sample xpath', [new DataNode()]) - then: 'the cps data service method to replace list content is invoked once with same parameters' - 1 * mockCpsDataService.replaceListContent(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xpath', [new DataNode()], NO_TIMESTAMP); - } - - def 'Delete data node via xPath'() { - when: 'Delete data node method is called with xpath as parameter' - objectUnderTest.deleteDataNode('sample dataNode xpath') - then: 'the cps data service method to delete data node is invoked once with the same xPath' - 1 * mockCpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'sample dataNode xpath', NO_TIMESTAMP); - } - - def 'Delete multiple data nodes via xPath'() { - when: 'Delete data nodes method is called with multiple xpaths as parameters' - objectUnderTest.deleteDataNodes(['xpath1', 'xpath2']) - then: 'the cps data service method to delete data nodes is invoked once with the same xPaths' - 1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP); - } - -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/DataSyncWatchdogSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/DataSyncWatchdogSpec.groovy deleted file mode 100644 index ed313a0ad8..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/DataSyncWatchdogSpec.groovy +++ /dev/null @@ -1,118 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 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.sync - -import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME - -import com.hazelcast.map.IMap -import org.onap.cps.api.CpsDataService -import org.onap.cps.ncmp.api.impl.inventory.sync.DataSyncWatchdog -import org.onap.cps.ncmp.api.impl.inventory.sync.SyncUtils -import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle -import org.onap.cps.ncmp.api.impl.inventory.CmHandleState -import org.onap.cps.ncmp.api.impl.inventory.CompositeState -import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence -import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState -import spock.lang.Specification - -class DataSyncWatchdogSpec extends Specification { - - def mockInventoryPersistence = Mock(InventoryPersistence) - - def mockCpsDataService = Mock(CpsDataService) - - def mockSyncUtils = Mock(SyncUtils) - - def mockDataSyncSemaphores = Mock(IMap) - - def jsonString = '{"stores:bookstore":{"categories":[{"code":"01"}]}}' - - def objectUnderTest = new DataSyncWatchdog(mockInventoryPersistence, mockCpsDataService, mockSyncUtils, mockDataSyncSemaphores) - - def compositeState = getCompositeState() - - def yangModelCmHandle1 = createSampleYangModelCmHandle('cm-handle-1') - - def yangModelCmHandle2 = createSampleYangModelCmHandle('cm-handle-2') - - def 'Data Sync for Cm Handle State in READY and Operational Sync State in UNSYNCHRONIZED.'() { - given: 'sample resource data' - def resourceData = jsonString - and: 'sync utilities returns a cm handle twice' - mockSyncUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1, yangModelCmHandle2] - when: 'data sync poll is executed' - objectUnderTest.executeUnSynchronizedReadyCmHandlePoll() - then: 'the inventory persistence cm handle returns a composite state for the first cm handle' - 1 * mockInventoryPersistence.getCmHandleState('cm-handle-1') >> compositeState - and: 'the sync util returns first resource data' - 1 * mockSyncUtils.getResourceData('cm-handle-1') >> resourceData - and: 'the cm-handle data is saved' - 1 * mockCpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cm-handle-1', jsonString, _) - and: 'the first cm handle operational sync state is updated' - 1 * mockInventoryPersistence.saveCmHandleState('cm-handle-1', compositeState) - then: 'the inventory persistence cm handle returns a composite state for the second cm handle' - 1 * mockInventoryPersistence.getCmHandleState('cm-handle-2') >> compositeState - and: 'the sync util returns first resource data' - 1 * mockSyncUtils.getResourceData('cm-handle-2') >> resourceData - and: 'the cm-handle data is saved' - 1 * mockCpsDataService.saveData(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cm-handle-2', jsonString, _) - and: 'the second cm handle operational sync state is updated from "UNSYNCHRONIZED" to "SYNCHRONIZED"' - 1 * mockInventoryPersistence.saveCmHandleState('cm-handle-2', compositeState) - } - - def 'Data Sync for Cm Handle State in READY and Operational Sync State in UNSYNCHRONIZED without resource data.'() { - given: 'sync utilities returns a cm handle' - mockSyncUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1] - when: 'data sync poll is executed' - objectUnderTest.executeUnSynchronizedReadyCmHandlePoll() - then: 'the inventory persistence cm handle returns a composite state for the first cm handle' - 1 * mockInventoryPersistence.getCmHandleState('cm-handle-1') >> compositeState - and: 'the sync util returns no resource data' - 1 * mockSyncUtils.getResourceData('cm-handle-1') >> null - and: 'the cm-handle data is not saved' - 0 * mockCpsDataService.saveData(*_) - } - - def 'Data Sync for Cm Handle that is already being processed.'() { - given: 'sync utilities returns a cm handle' - mockSyncUtils.getUnsynchronizedReadyCmHandles() >> [yangModelCmHandle1] - and: 'the shared data sync semaphore indicate it is already being processed' - mockDataSyncSemaphores.putIfAbsent('cm-handle-1', _, _, _) >> 'something (not null)' - when: 'data sync poll is executed' - objectUnderTest.executeUnSynchronizedReadyCmHandlePoll() - then: 'it is NOT processed e.g. state is not requested' - 0 * mockInventoryPersistence.getCmHandleState(*_) - } - - def createSampleYangModelCmHandle(cmHandleId) { - def compositeState = getCompositeState() - return new YangModelCmHandle(id: cmHandleId, compositeState: compositeState) - } - - def getCompositeState() { - def cmHandleState = CmHandleState.READY - def compositeState = new CompositeState(cmHandleState: cmHandleState) - compositeState.setDataStores(CompositeState.DataStores.builder() - .operationalDataStore(CompositeState.Operational.builder().dataStoreSyncState(DataStoreSyncState.SYNCHRONIZED) - .build()).build()) - return compositeState - } -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncServiceSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncServiceSpec.groovy deleted file mode 100644 index a13f595200..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncServiceSpec.groovy +++ /dev/null @@ -1,155 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 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.sync - -import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.LOCKED_MISBEHAVING -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.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME -import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_UPGRADE - -import org.onap.cps.ncmp.api.impl.inventory.CmHandleState -import org.onap.cps.spi.FetchDescendantsOption -import org.onap.cps.spi.model.DataNode -import org.onap.cps.api.CpsAdminService -import org.onap.cps.api.CpsDataService -import org.onap.cps.api.CpsModuleService -import org.onap.cps.ncmp.api.impl.inventory.sync.ModuleSyncService -import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations -import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle -import org.onap.cps.ncmp.api.impl.inventory.CmHandleQueries -import org.onap.cps.ncmp.api.impl.inventory.CompositeStateBuilder -import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle -import org.onap.cps.spi.CascadeDeleteAllowed -import org.onap.cps.spi.exceptions.SchemaSetNotFoundException -import org.onap.cps.spi.model.ModuleReference -import org.onap.cps.utils.JsonObjectMapper -import spock.lang.Specification - -class ModuleSyncServiceSpec extends Specification { - - def mockCpsModuleService = Mock(CpsModuleService) - def mockDmiModelOperations = Mock(DmiModelOperations) - def mockCpsAdminService = Mock(CpsAdminService) - def mockCmHandleQueries = Mock(CmHandleQueries) - def mockCpsDataService = Mock(CpsDataService) - def mockJsonObjectMapper = Mock(JsonObjectMapper) - - def objectUnderTest = new ModuleSyncService(mockDmiModelOperations, mockCpsModuleService, mockCpsAdminService, - mockCmHandleQueries, mockCpsDataService, mockJsonObjectMapper) - - def expectedDataspaceName = NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME - - def 'Sync model for a (new) cm handle with #scenario'() { - given: 'a cm handle' - def ncmpServiceCmHandle = new NcmpServiceCmHandle() - ncmpServiceCmHandle.setCompositeState(new CompositeStateBuilder().build()) - def dmiServiceName = 'some service name' - ncmpServiceCmHandle.cmHandleId = 'cmHandleId-1' - def yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle(dmiServiceName, '', '', ncmpServiceCmHandle,'') - and: 'DMI operations returns some module references' - def moduleReferences = [ new ModuleReference('module1','1'), new ModuleReference('module2','2') ] - mockDmiModelOperations.getModuleReferences(yangModelCmHandle) >> moduleReferences - and: 'CPS-Core returns list of existing module resources' - mockCpsModuleService.getYangResourceModuleReferences(expectedDataspaceName) >> toModuleReference(existingModuleResourcesInCps) - and: 'DMI-Plugin returns resource(s) for "new" module(s)' - mockDmiModelOperations.getNewYangResourcesFromDmi(yangModelCmHandle, [new ModuleReference('module1', '1')]) >> newModuleNameContentToMap - when: 'module sync is triggered' - mockCpsModuleService.identifyNewModuleReferences(moduleReferences) >> toModuleReference(identifiedNewModuleReferences) - objectUnderTest.syncAndCreateOrUpgradeSchemaSetAndAnchor(yangModelCmHandle) - then: 'create schema set from module is invoked with correct parameters' - 1 * mockCpsModuleService.createOrUpgradeSchemaSetFromModules(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cmHandleId-1', newModuleNameContentToMap, moduleReferences) - and: 'anchor is created with the correct parameters' - 1 * mockCpsAdminService.createAnchor(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cmHandleId-1', 'cmHandleId-1') - where: 'the following parameters are used' - scenario | existingModuleResourcesInCps | identifiedNewModuleReferences | newModuleNameContentToMap - 'one new module' | [['module2' : '2'], ['module3' : '3']] | [['module1' : '1']] | [module1: 'some yang source'] - 'no add. properties' | [['module2' : '2'], ['module3' : '3']] | [['module1' : '1']] | [module1: 'some yang source'] - 'no new module' | [['module1' : '1'], ['module2' : '2']] | [] | [:] - } - - def 'upgrade model for a existing cm handle'() { - given: 'a cm handle that is ready but locked for upgrade' - def ncmpServiceCmHandle = new NcmpServiceCmHandle() - ncmpServiceCmHandle.setCompositeState(new CompositeStateBuilder() - .withLockReason(MODULE_UPGRADE, 'new moduleSetTag: targetModuleSetTag').build()) - ncmpServiceCmHandle.setCmHandleId('cmHandleId-1') - def yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle('some service name', '', '', ncmpServiceCmHandle, 'targetModuleSetTag') - mockCmHandleQueries.cmHandleHasState('cmHandleId-1', CmHandleState.READY) >> true - and: 'the module service returns some module references' - def moduleReferences = [new ModuleReference('module1', '1'), new ModuleReference('module2', '2')] - mockCpsModuleService.getYangResourcesModuleReferences(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR) >> moduleReferences - and: 'a cm handle with the same moduleSetTag can be found in the registry' - mockCmHandleQueries.queryNcmpRegistryByCpsPath("//cm-handles[@module-set-tag='targetModuleSetTag']", - FetchDescendantsOption.OMIT_DESCENDANTS) >> [new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'cmHandleId-1\']', leaves: ['id': 'cmHandleId-1', 'cm-handle-state': 'READY'])] - when: 'module upgrade is triggered' - objectUnderTest.syncAndCreateOrUpgradeSchemaSetAndAnchor(yangModelCmHandle) - then: 'the upgrade is delegated to the module service (with the correct parameters)' - 1 * mockCpsModuleService.createOrUpgradeSchemaSetFromModules(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'cmHandleId-1', Collections.emptyMap(), moduleReferences) - } - - def 'Delete Schema Set for CmHandle'() { - when: 'delete schema set if exists is called' - objectUnderTest.deleteSchemaSetIfExists('some-cmhandle-id') - then: 'the module service is invoked to delete the correct schema set' - 1 * mockCpsModuleService.deleteSchemaSet(expectedDataspaceName, 'some-cmhandle-id', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED) - } - - def 'Delete a non-existing Schema Set for CmHandle' () { - given: 'the DB throws an exception because its Schema Set does not exist' - mockCpsModuleService.deleteSchemaSet(*_) >> { throw new SchemaSetNotFoundException('some-dataspace-name', 'some-cmhandle-id') } - when: 'delete schema set if exists is called' - objectUnderTest.deleteSchemaSetIfExists('some-cmhandle-id') - then: 'the exception from the DB is ignored; there are no exceptions' - noExceptionThrown() - } - - def 'Delete Schema Set for CmHandle with other exception' () { - given: 'an exception other than SchemaSetNotFoundException is thrown' - UnsupportedOperationException unsupportedOperationException = new UnsupportedOperationException(); - 1 * mockCpsModuleService.deleteSchemaSet(*_) >> { throw unsupportedOperationException } - when: 'delete schema set if exists is called' - objectUnderTest.deleteSchemaSetIfExists('some-cmhandle-id') - then: 'an exception is thrown' - def result = thrown(UnsupportedOperationException) - result == unsupportedOperationException - } - - def 'Extract module set tag with #scenario'() { - expect: 'the module set tag is extracted correctly' - assert expectedModuleSetTag == objectUnderTest.extractModuleSetTag(new CompositeStateBuilder().withLockReason(lockReason, 'new moduleSetTag: targetModuleSetTag').build()) - where: 'the following parameters are used' - scenario | lockReason || expectedModuleSetTag - 'locked reason is null' | null || '' - 'locked for other reason' | LOCKED_MISBEHAVING || '' - 'locked for module upgrade' | MODULE_UPGRADE || 'targetModuleSetTag' - } - - def toModuleReference(moduleReferenceAsMap) { - def moduleReferences = [].withDefault { [:] } - moduleReferenceAsMap.forEach(property -> - property.forEach((moduleName, revision) -> { - moduleReferences.add(new ModuleReference('moduleName' : moduleName, 'revision' : revision)) - })) - return moduleReferences - } - -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncTasksSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncTasksSpec.groovy deleted file mode 100644 index 8698136d65..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncTasksSpec.groovy +++ /dev/null @@ -1,217 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 Nordix Foundation - * 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. - * 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.sync - -import ch.qos.logback.classic.Level -import ch.qos.logback.classic.Logger -import ch.qos.logback.classic.spi.ILoggingEvent -import ch.qos.logback.core.read.ListAppender -import com.hazelcast.config.Config -import com.hazelcast.instance.impl.HazelcastInstanceFactory -import com.hazelcast.map.IMap -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.onap.cps.ncmp.api.impl.events.lcm.LcmEventsCmHandleStateHandler -import org.onap.cps.ncmp.api.impl.inventory.sync.ModuleSyncService -import org.onap.cps.ncmp.api.impl.inventory.sync.ModuleSyncTasks -import org.onap.cps.ncmp.api.impl.inventory.sync.SyncUtils -import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle -import org.onap.cps.ncmp.api.impl.inventory.CmHandleState -import org.onap.cps.ncmp.api.impl.inventory.CompositeState -import org.onap.cps.ncmp.api.impl.inventory.CompositeStateBuilder -import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence -import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory -import org.onap.cps.spi.model.DataNode -import org.slf4j.LoggerFactory -import spock.lang.Specification -import java.util.concurrent.atomic.AtomicInteger - -class ModuleSyncTasksSpec extends Specification { - - def logger = Spy(ListAppender) - - @BeforeEach - void setup() { - ((Logger) LoggerFactory.getLogger(ModuleSyncTasks.class)).addAppender(logger); - logger.start(); - } - - @AfterEach - void teardown() { - ((Logger) LoggerFactory.getLogger(ModuleSyncTasks.class)).detachAndStopAllAppenders(); - } - - def mockInventoryPersistence = Mock(InventoryPersistence) - - def mockSyncUtils = Mock(SyncUtils) - - def mockModuleSyncService = Mock(ModuleSyncService) - - def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler) - - IMap moduleSyncStartedOnCmHandles = HazelcastInstanceFactory - .getOrCreateHazelcastInstance(new Config('hazelcastInstanceName')) - .getMap('mapInstanceName') - - def batchCount = new AtomicInteger(5) - - def objectUnderTest = new ModuleSyncTasks(mockInventoryPersistence, mockSyncUtils, mockModuleSyncService, - mockLcmEventsCmHandleStateHandler, moduleSyncStartedOnCmHandles) - - def 'Module Sync ADVISED cm handles.'() { - given: 'cm handles in an ADVISED state' - def cmHandle1 = advisedCmHandleAsDataNode('cm-handle-1') - def cmHandle2 = advisedCmHandleAsDataNode('cm-handle-2') - and: 'the inventory persistence cm handle returns a ADVISED state for the any handle' - mockInventoryPersistence.getCmHandleState(_) >> new CompositeState(cmHandleState: CmHandleState.ADVISED) - when: 'module sync poll is executed' - objectUnderTest.performModuleSync([cmHandle1, cmHandle2], batchCount) - then: 'module sync service deletes schemas set of each cm handle if it already exists' - 1 * mockModuleSyncService.deleteSchemaSetIfExists('cm-handle-1') - 1 * mockModuleSyncService.deleteSchemaSetIfExists('cm-handle-2') - and: 'module sync service is invoked for each cm handle' - 1 * mockModuleSyncService.syncAndCreateOrUpgradeSchemaSetAndAnchor(_) >> { args -> assertYamgModelCmHandleArgument(args, 'cm-handle-1') } - 1 * mockModuleSyncService.syncAndCreateOrUpgradeSchemaSetAndAnchor(_) >> { args -> assertYamgModelCmHandleArgument(args, 'cm-handle-2') } - and: 'the state handler is called for the both cm handles' - 1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >> { args -> - assertBatch(args, ['cm-handle-1', 'cm-handle-2'], CmHandleState.READY) - } - and: 'batch count is decremented by one' - assert batchCount.get() == 4 - } - - def 'Module Sync ADVISED cm handle with failure during sync.'() { - given: 'a cm handle in an ADVISED state' - def cmHandle = advisedCmHandleAsDataNode('cm-handle') - and: 'the inventory persistence cm handle returns a ADVISED state for the cm handle' - def cmHandleState = new CompositeState(cmHandleState: CmHandleState.ADVISED) - 1 * mockInventoryPersistence.getCmHandleState('cm-handle') >> cmHandleState - and: 'module sync service attempts to sync the cm handle and throws an exception' - 1 * mockModuleSyncService.syncAndCreateOrUpgradeSchemaSetAndAnchor(*_) >> { throw new Exception('some exception') } - when: 'module sync is executed' - objectUnderTest.performModuleSync([cmHandle], batchCount) - then: 'update lock reason, details and attempts is invoked' - 1 * mockSyncUtils.updateLockReasonDetailsAndAttempts(cmHandleState, LockReasonCategory.MODULE_SYNC_FAILED, 'some exception') - and: 'the state handler is called to update the state to LOCKED' - 1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >> { args -> - assertBatch(args, ['cm-handle'], CmHandleState.LOCKED) - } - and: 'batch count is decremented by one' - assert batchCount.get() == 4 - } - - def 'Reset failed CM Handles #scenario.'() { - given: 'cm handles in an locked state' - def lockedState = new CompositeStateBuilder().withCmHandleState(CmHandleState.LOCKED) - .withLockReason(LockReasonCategory.MODULE_SYNC_FAILED, '').withLastUpdatedTimeNow().build() - def yangModelCmHandle1 = new YangModelCmHandle(id: 'cm-handle-1', compositeState: lockedState) - def yangModelCmHandle2 = new YangModelCmHandle(id: 'cm-handle-2', compositeState: lockedState) - def expectedCmHandleStatePerCmHandle = [(yangModelCmHandle1): CmHandleState.ADVISED] - and: 'clear in progress map' - resetModuleSyncStartedOnCmHandles(moduleSyncStartedOnCmHandles) - and: 'add cm handle entry into progress map' - moduleSyncStartedOnCmHandles.put('cm-handle-1', 'started') - moduleSyncStartedOnCmHandles.put('cm-handle-2', 'started') - and: 'sync utils retry locked cm handle returns #isReadyForRetry' - mockSyncUtils.needsModuleSyncRetryOrUpgrade(lockedState) >>> isReadyForRetry - when: 'resetting failed cm handles' - objectUnderTest.resetFailedCmHandles([yangModelCmHandle1, yangModelCmHandle2]) - then: 'updated to state "ADVISED" from "READY" is called as often as there are cm handles ready for retry' - expectedNumberOfInvocationsToUpdateCmHandleState * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(expectedCmHandleStatePerCmHandle) - and: 'after reset performed size of in progress map' - assert moduleSyncStartedOnCmHandles.size() == inProgressMapSize - where: - scenario | isReadyForRetry | inProgressMapSize || expectedNumberOfInvocationsToUpdateCmHandleState - 'retry locked cm handle' | [true, false] | 1 || 1 - 'do not retry locked cm handle' | [false, false] | 2 || 0 - } - - def 'Module Sync ADVISED cm handle without entry in progress map.'() { - given: 'cm handles in an ADVISED state' - def cmHandle1 = advisedCmHandleAsDataNode('cm-handle-1') - and: 'the inventory persistence cm handle returns a ADVISED state for the any handle' - mockInventoryPersistence.getCmHandleState(_) >> new CompositeState(cmHandleState: CmHandleState.ADVISED) - and: 'entry in progress map for other cm handle' - moduleSyncStartedOnCmHandles.put('other-cm-handle', 'started') - when: 'module sync poll is executed' - objectUnderTest.performModuleSync([cmHandle1], batchCount) - then: 'module sync service is invoked for cm handle' - 1 * mockModuleSyncService.syncAndCreateOrUpgradeSchemaSetAndAnchor(_) >> { args -> assertYamgModelCmHandleArgument(args, 'cm-handle-1') } - and: 'the entry for other cm handle is still in the progress map' - assert moduleSyncStartedOnCmHandles.get('other-cm-handle') != null - } - - def 'Remove already processed cm handle id from hazelcast map'() { - given: 'hazelcast map contains cm handle id' - moduleSyncStartedOnCmHandles.put('ch-1', 'started') - when: 'remove cm handle entry' - objectUnderTest.removeResetCmHandleFromModuleSyncMap('ch-1') - then: 'an event is logged with level INFO' - def loggingEvent = getLoggingEvent() - assert loggingEvent.level == Level.INFO - and: 'the log indicates the cm handle entry is removed successfully' - assert loggingEvent.formattedMessage == 'ch-1 removed from in progress map' - } - - def 'Remove non-existing cm handle id from hazelcast map'() { - given: 'hazelcast map does not contains cm handle id' - def result = moduleSyncStartedOnCmHandles.get('non-existing-cm-handle') - assert result == null - when: 'remove cm handle entry from hazelcast map' - objectUnderTest.removeResetCmHandleFromModuleSyncMap('non-existing-cm-handle') - then: 'no event is logged' - def loggingEvent = getLoggingEvent() - assert loggingEvent == null - } - - def advisedCmHandleAsDataNode(cmHandleId) { - return new DataNode(anchorName: cmHandleId, leaves: ['id': cmHandleId, 'cm-handle-state': 'ADVISED']) - } - - def assertYamgModelCmHandleArgument(args, expectedCmHandleId) { - { - def yangModelCmHandle = args[0] - assert yangModelCmHandle.id == expectedCmHandleId - } - return true - } - - def assertBatch(args, expectedCmHandleStatePerCmHandleIds, expectedCmHandleState) { - { - Map actualCmHandleStatePerCmHandle = args[0] - assert actualCmHandleStatePerCmHandle.size() == expectedCmHandleStatePerCmHandleIds.size() - actualCmHandleStatePerCmHandle.each { - assert expectedCmHandleStatePerCmHandleIds.contains(it.key.id) - assert it.value == expectedCmHandleState - } - } - return true - } - - def resetModuleSyncStartedOnCmHandles(moduleSyncStartedOnCmHandles) { - moduleSyncStartedOnCmHandles.clear(); - } - - def getLoggingEvent() { - return logger.list[0] - } -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncWatchdogSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncWatchdogSpec.groovy deleted file mode 100644 index 390e88b3d4..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/ModuleSyncWatchdogSpec.groovy +++ /dev/null @@ -1,126 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 Nordix Foundation - * 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. - * 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.sync - -import com.hazelcast.map.IMap -import org.onap.cps.ncmp.api.impl.inventory.sync.ModuleSyncTasks -import org.onap.cps.ncmp.api.impl.inventory.sync.ModuleSyncWatchdog -import org.onap.cps.ncmp.api.impl.inventory.sync.SyncUtils -import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle -import org.onap.cps.ncmp.api.impl.inventory.sync.executor.AsyncTaskExecutor -import java.util.concurrent.ArrayBlockingQueue -import org.onap.cps.spi.model.DataNode -import spock.lang.Specification - -class ModuleSyncWatchdogSpec extends Specification { - - def mockSyncUtils = Mock(SyncUtils) - - def static testQueueCapacity = 50 + 2 * ModuleSyncWatchdog.MODULE_SYNC_BATCH_SIZE - - def moduleSyncWorkQueue = new ArrayBlockingQueue(testQueueCapacity) - - def mockModuleSyncStartedOnCmHandles = Mock(IMap) - - def mockModuleSyncTasks = Mock(ModuleSyncTasks) - - def spiedAsyncTaskExecutor = Spy(AsyncTaskExecutor) - - def moduleSetTagCache = Mock(IMap>) - - def objectUnderTest = new ModuleSyncWatchdog(mockSyncUtils, moduleSyncWorkQueue , mockModuleSyncStartedOnCmHandles, mockModuleSyncTasks, spiedAsyncTaskExecutor, moduleSetTagCache) - - void setup() { - spiedAsyncTaskExecutor.setupThreadPool() - } - - def 'Module sync advised cm handles with #scenario.'() { - given: 'sync utilities returns #numberOfAdvisedCmHandles advised cm handles' - mockSyncUtils.getAdvisedCmHandles() >> createDataNodes(numberOfAdvisedCmHandles) - and: 'the executor has enough available threads' - spiedAsyncTaskExecutor.getAsyncTaskParallelismLevel() >> 3 - when: ' module sync is started' - objectUnderTest.moduleSyncAdvisedCmHandles() - then: 'it performs #expectedNumberOfTaskExecutions tasks' - expectedNumberOfTaskExecutions * spiedAsyncTaskExecutor.executeTask(*_) - where: 'the following parameter are used' - scenario | numberOfAdvisedCmHandles || expectedNumberOfTaskExecutions - 'less then 1 batch' | 1 || 1 - 'exactly 1 batch' | ModuleSyncWatchdog.MODULE_SYNC_BATCH_SIZE || 1 - '2 batches' | 2 * ModuleSyncWatchdog.MODULE_SYNC_BATCH_SIZE || 2 - 'queue capacity' | testQueueCapacity || 3 - 'over queue capacity' | testQueueCapacity + 2 * ModuleSyncWatchdog.MODULE_SYNC_BATCH_SIZE || 3 - } - - def 'Module sync advised cm handles starts with no available threads.'() { - given: 'sync utilities returns a advise cm handles' - mockSyncUtils.getAdvisedCmHandles() >> createDataNodes(1) - and: 'the executor first has no threads but has one thread on the second attempt' - spiedAsyncTaskExecutor.getAsyncTaskParallelismLevel() >>> [ 0, 1 ] - when: ' module sync is started' - objectUnderTest.moduleSyncAdvisedCmHandles() - then: 'it performs one task' - 1 * spiedAsyncTaskExecutor.executeTask(*_) - } - - def 'Module sync advised cm handles already handled.'() { - given: 'sync utilities returns a advise cm handles' - mockSyncUtils.getAdvisedCmHandles() >> createDataNodes(1) - and: 'the executor has a thread available' - spiedAsyncTaskExecutor.getAsyncTaskParallelismLevel() >> 1 - and: 'the semaphore cache indicates the cm handle is already being processed' - mockModuleSyncStartedOnCmHandles.putIfAbsent(*_) >> 'Started' - when: ' module sync is started' - objectUnderTest.moduleSyncAdvisedCmHandles() - then: 'it does NOT execute a task to process the (empty) batch' - 0 * spiedAsyncTaskExecutor.executeTask(*_) - } - - def 'Module sync with previous cm handle(s) left in work queue.'() { - given: 'there is still a cm handle in the queue' - moduleSyncWorkQueue.offer(new DataNode()) - and: 'sync utilities returns many advise cm handles' - mockSyncUtils.getAdvisedCmHandles() >> createDataNodes(500) - and: 'the executor has plenty threads available' - spiedAsyncTaskExecutor.getAsyncTaskParallelismLevel() >> 10 - when: ' module sync is started' - objectUnderTest.moduleSyncAdvisedCmHandles() - then: 'it does executes only one task to process the remaining handle in the queue' - 1 * spiedAsyncTaskExecutor.executeTask(*_) - } - - def 'Reset failed cm handles.'() { - given: 'sync utilities returns failed cm handles' - def failedCmHandles = [new YangModelCmHandle()] - mockSyncUtils.getCmHandlesThatFailedModelSyncOrUpgrade() >> failedCmHandles - when: 'reset failed cm handles is started' - objectUnderTest.resetPreviouslyFailedCmHandles() - then: 'it is delegated to the module sync task (service)' - 1 * mockModuleSyncTasks.resetFailedCmHandles(failedCmHandles) - } - - def createDataNodes(numberOfDataNodes) { - def dataNodes = [] - (1..numberOfDataNodes).each {dataNodes.add(new DataNode())} - return dataNodes - } -} 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 deleted file mode 100644 index df2ad711ed..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/SyncUtilsSpec.groovy +++ /dev/null @@ -1,198 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 Nordix Foundation - * 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. - * 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.sync - -import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.LOCKED_MISBEHAVING -import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL -import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_SYNC_FAILED -import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_UPGRADE -import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_UPGRADE_FAILED - -import ch.qos.logback.classic.Level -import ch.qos.logback.classic.Logger -import ch.qos.logback.core.read.ListAppender -import org.onap.cps.ncmp.api.impl.inventory.sync.SyncUtils -import org.slf4j.LoggerFactory -import org.springframework.context.annotation.AnnotationConfigApplicationContext -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.inventory.CmHandleQueries -import org.onap.cps.ncmp.api.impl.inventory.CmHandleState -import org.onap.cps.ncmp.api.impl.inventory.CompositeState -import org.onap.cps.ncmp.api.impl.inventory.CompositeStateBuilder -import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState -import org.onap.cps.spi.FetchDescendantsOption -import org.onap.cps.spi.model.DataNode -import org.onap.cps.utils.JsonObjectMapper -import org.springframework.http.HttpStatus -import org.springframework.http.ResponseEntity -import spock.lang.Specification -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter -import java.util.stream.Collectors - -class SyncUtilsSpec extends Specification{ - - def mockCmHandleQueries = Mock(CmHandleQueries) - - def mockDmiDataOperations = Mock(DmiDataOperations) - - def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) - - def objectUnderTest = new SyncUtils(mockCmHandleQueries, mockDmiDataOperations, jsonObjectMapper) - - def static neverUpdatedBefore = '1900-01-01T00:00:00.000+0100' - - def static now = OffsetDateTime.now() - - def static nowAsString = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(now) - - def static dataNode = new DataNode(leaves: ['id': 'cm-handle-123']) - - def applicationContext = new AnnotationConfigApplicationContext() - - def logger = (Logger) LoggerFactory.getLogger(SyncUtils) - def loggingListAppender - - void setup() { - logger.setLevel(Level.DEBUG) - loggingListAppender = new ListAppender() - logger.addAppender(loggingListAppender) - loggingListAppender.start() - applicationContext.refresh() - } - - void cleanup() { - ((Logger) LoggerFactory.getLogger(SyncUtils.class)).detachAndStopAllAppenders() - applicationContext.close() - } - - def 'Get an advised Cm-Handle where ADVISED cm handle #scenario'() { - given: 'the inventory persistence service returns a collection of data nodes' - mockCmHandleQueries.queryCmHandlesByState(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 - where: 'the following scenarios are used' - scenario | dataNodeCollection || expectedCallsToGetYangModelCmHandle | expectedDataNodeSize - 'exists' | [dataNode] || 1 | 1 - 'does not exist' | [] || 0 | 0 - } - - def 'Update Lock Reason, Details and Attempts where lock reason #scenario'() { - given: 'A locked state' - def compositeState = new CompositeState(lockReason: lockReason) - when: 'update cm handle details and attempts is called' - objectUnderTest.updateLockReasonDetailsAndAttempts(compositeState, MODULE_SYNC_FAILED, 'new error message') - then: 'the composite state lock reason and details are updated' - assert compositeState.lockReason.lockReasonCategory == MODULE_SYNC_FAILED - assert compositeState.lockReason.details == expectedDetails - where: - scenario | lockReason || expectedDetails - 'does not exist' | null || 'Attempt #1 failed: new error message' - 'exists' | CompositeState.LockReason.builder().details("Attempt #2 failed: some error message").build() || 'Attempt #3 failed: new error message' - } - - def 'Get all locked Cm-Handle where Lock Reason is MODULE_SYNC_FAILED cm handle #scenario'() { - given: 'the cps (persistence service) returns a collection of data nodes' - mockCmHandleQueries.queryCmHandleAncestorsByCpsPath( - '//lock-reason[@reason="MODULE_SYNC_FAILED" or @reason="MODULE_UPGRADE"]', - FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode] - when: 'get locked Misbehaving cm handle is called' - def result = objectUnderTest.getCmHandlesThatFailedModelSyncOrUpgrade() - then: 'the returned cm handle collection is the correct size' - result.size() == 1 - and: 'the correct cm handle is returned' - result[0].id == 'cm-handle-123' - } - - def 'Retry Locked Cm-Handle where the last update time is #scenario'() { - given: 'Last update was #lastUpdateMinutesAgo minutes ago (-1 means never)' - def lastUpdatedTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(now.minusMinutes(lastUpdateMinutesAgo)) - if (lastUpdateMinutesAgo < 0 ) { - lastUpdatedTime = neverUpdatedBefore - } - when: 'checking to see if cm handle is ready for retry' - def result = objectUnderTest.needsModuleSyncRetryOrUpgrade(new CompositeStateBuilder() - .withLockReason(MODULE_SYNC_FAILED, lockDetails) - .withLastUpdatedTime(lastUpdatedTime).build()) - then: 'retry is only attempted when expected' - assert result == retryExpected - and: 'logs contain related information' - def logs = loggingListAppender.list.toString() - assert logs.contains(logReason) - where: 'the following parameters are used' - scenario | lastUpdateMinutesAgo | lockDetails | logReason || retryExpected - 'never attempted before' | -1 | 'fist attempt:' | 'First Attempt:' || true - '1st attempt, last attempt > 2 minute ago' | 3 | 'Attempt #1 failed:' | 'Retry due now' || true - '2nd attempt, last attempt < 4 minutes ago' | 1 | 'Attempt #2 failed:' | 'Time until next attempt is 3 minutes:' || false - '2nd attempt, last attempt > 4 minutes ago' | 5 | 'Attempt #2 failed:' | 'Retry due now' || true - } - - def 'Retry Locked Cm-Handle with other lock reasons (category) #lockReasonCategory'() { - when: 'checking to see if cm handle is ready for retry' - def result = objectUnderTest.needsModuleSyncRetryOrUpgrade(new CompositeStateBuilder() - .withLockReason(lockReasonCategory, 'some details') - .withLastUpdatedTime(nowAsString).build()) - then: 'verify retry attempts' - assert result == retryAttempt - and: 'logs contain related information' - def logs = loggingListAppender.list.toString() - assert logs.contains(logReason) - where: 'the following lock reasons occurred' - scenario | lockReasonCategory || logReason | retryAttempt - 'module upgrade' | MODULE_UPGRADE || 'Locked for module upgrade.' | true - 'module sync failed' | MODULE_SYNC_FAILED || 'First Attempt:' | false - 'lock misbehaving' | LOCKED_MISBEHAVING || 'Locked for other reason' | false - } - - def 'Get a Cm-Handle where #scenario'() { - given: 'the inventory persistence service returns a collection of data nodes' - mockCmHandleQueries.queryCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) >> unSynchronizedDataNodes - mockCmHandleQueries.cmHandleHasState('cm-handle-123', CmHandleState.READY) >> cmHandleHasState - when: 'get advised cm handles are fetched' - def yangModelCollection = objectUnderTest.getUnsynchronizedReadyCmHandles() - then: 'the returned data node collection is the correct size' - yangModelCollection.size() == expectedDataNodeSize - and: 'the result contains the correct data' - yangModelCollection.stream().map(yangModel -> yangModel.id).collect(Collectors.toSet()) == expectedYangModelCollectionIds - where: 'the following scenarios are used' - scenario | unSynchronizedDataNodes | cmHandleHasState || expectedDataNodeSize | expectedYangModelCollectionIds - 'a Cm-Handle unsynchronized and ready' | [dataNode] | true || 1 | ['cm-handle-123'] as Set - 'a Cm-Handle unsynchronized but not ready' | [dataNode] | false || 0 | [] as Set - 'all Cm-Handle synchronized' | [] | false || 0 | [] as Set - } - - def 'Get resource data through DMI Operations #scenario'() { - given: 'the inventory persistence service returns a collection of data nodes' - def jsonString = '{"stores:bookstore":{"categories":[{"code":"01"}]}}' - JsonNode jsonNode = jsonObjectMapper.convertToJsonNode(jsonString); - def responseEntity = new ResponseEntity<>(jsonNode, HttpStatus.OK) - mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_OPERATIONAL.datastoreName, 'cm-handle-123', _) >> responseEntity - when: 'get resource data is called' - def result = objectUnderTest.getResourceData('cm-handle-123') - then: 'the returned data is correct' - result == jsonString - } -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/config/WatchdogSchedulingConfigurerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/config/WatchdogSchedulingConfigurerSpec.groovy deleted file mode 100644 index 7361948bc6..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/config/WatchdogSchedulingConfigurerSpec.groovy +++ /dev/null @@ -1,59 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 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.sync.config - -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.onap.cps.ncmp.api.impl.inventory.sync.config.WatchdogSchedulingConfigurer -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.context.ConfigurableApplicationContext -import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification - -@SpringBootTest -@ContextConfiguration(classes = [ConfigurableApplicationContext, WatchdogSchedulingConfigurer]) -class WatchdogSchedulingConfigurerSpec extends Specification { - - @Autowired - private ConfigurableApplicationContext applicationContext; - - def watchdogSchedulingConfigurer; - - @BeforeEach - void setup() { - watchdogSchedulingConfigurer = (WatchdogSchedulingConfigurer) applicationContext.getBean("watchdogSchedulingConfigurer") - } - - @AfterEach - void tearDown() { - if (applicationContext != null) { - applicationContext.close() - } - } - - def 'Validate watchdog scheduling configuration'() { - given: 'task scheduler configuration properties are loaded as map' - def linkedHashMap = watchdogSchedulingConfigurer.taskScheduler().getProperties() - expect: 'thread name prefix is mapped correctly' - assert linkedHashMap.'threadNamePrefix' == 'watchdog-th-' - } -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/executor/AsyncTaskExecutorSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/executor/AsyncTaskExecutorSpec.groovy deleted file mode 100644 index b3ed94520a..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/executor/AsyncTaskExecutorSpec.groovy +++ /dev/null @@ -1,62 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 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.sync.executor - -import org.onap.cps.ncmp.api.impl.inventory.sync.executor.AsyncTaskExecutor -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import spock.lang.Specification -import java.util.concurrent.TimeoutException -import java.util.function.Supplier - -@SpringBootTest(classes = AsyncTaskExecutor) -class AsyncTaskExecutorSpec extends Specification { - - @Autowired - AsyncTaskExecutor objectUnderTest - def mockTaskSupplier = Mock(Supplier) - - def 'Parallelism level configuration.'() { - expect: 'Parallelism level is configured with the correct value' - assert objectUnderTest.getAsyncTaskParallelismLevel() == 3 - } - - def 'Task completion with #caseDescriptor.'() { - when: 'task completion is handled' - def irrelevantResponse = null - objectUnderTest.handleTaskCompletion(irrelevantResponse, exception); - then: 'any exception is swallowed by the task completion (logged)' - noExceptionThrown() - where: 'following cases are tested' - caseDescriptor | exception - 'no exception' | null - 'time out exception' | new TimeoutException("time-out") - 'unexpected exception' | new Exception("some exception") - } - - def 'Task execution.'() { - when: 'a task is submitted for execution' - objectUnderTest.executeTask(() -> mockTaskSupplier, 0) - then: 'the task submission is successful' - noExceptionThrown() - } - -} -- cgit 1.2.3-korg