diff --git a/build-locally.sh b/build-locally.sh index 02f9b768..e6caf23f 100755 --- a/build-locally.sh +++ b/build-locally.sh @@ -70,7 +70,6 @@ Environment variables used: DEBUG - Optional. Valid values "1" (on) or "0" (off). Defaults to "0" (off). SOURCE_MAVEN - Optional. Where maven/gradle can look for pre-built development levels of things. Defaults to https://development.galasa.dev/main/maven-repo/framework/ -LOGS_DIR - Optional. Where logs are placed. Defaults to creating a temporary directory. EOF } @@ -140,17 +139,8 @@ else info "SOURCE_MAVEN set to ${SOURCE_MAVEN} by caller." fi -# Create a temporary dir. -# Note: This bash 'spell' works in OSX and Linux. -if [[ -z ${LOGS_DIR} ]]; then - export LOGS_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t "galasa-logs") - info "Logs are stored in the ${LOGS_DIR} folder." - info "Over-ride this setting using the LOGS_DIR environment variable." -else - mkdir -p ${LOGS_DIR} 2>&1 > /dev/null # Don't show output. We don't care if it already existed. - info "Logs are stored in the ${LOGS_DIR} folder." - info "Over-ridden by caller using the LOGS_DIR variable." -fi +export LOGS_DIR=$BASEDIR/temp +mkdir -p $LOGS_DIR info "Using source code at ${source_dir}" cd ${BASEDIR}/${source_dir} diff --git a/galasa-extensions-parent/dev.galasa.cps.rest/README.md b/galasa-extensions-parent/dev.galasa.cps.rest/README.md index 91271af2..ada452fc 100644 --- a/galasa-extensions-parent/dev.galasa.cps.rest/README.md +++ b/galasa-extensions-parent/dev.galasa.cps.rest/README.md @@ -9,7 +9,7 @@ The configuration on the ecosystem is read-only. Set and Delete operations are n To configure Galasa to load and use this adapter: -The galasactl tool can be configured to communicate with that CPS (see the latest docs on the galasactl tool. +The galasactl tool can be configured to communicate with that CPS (see the latest docs on the galasactl tool). To do this, assuming `https://myhost/api/bootstrap` can be used to communicate with the remote server, add the following to your `bootstrap.properties` file, @@ -18,4 +18,13 @@ communicate with the remote server, add the following to your `bootstrap.propert # https://myhost/api is the location of the Galasa REST API endpoints. framework.config.store=galasacps://myhost/api # Tells the framework to load this extension, so it can register to react when the `galasacps` URL scheme is used. -framework.extra.bundles=dev.galasa.cps.rest \ No newline at end of file +framework.extra.bundles=dev.galasa.cps.rest +``` + +The CPS over REST feature has a cache which can be turned on using the `framework.cps.rest.cache.is.enabled` property. +- Set it to `true` to enable caching of CPS properties on the client-side, with an agressive cache-priming which loads all +CPS properties into the cache at the start. +- Set it to `false` or don't have that property in your CPS store, and the caching will be disabled. + + + diff --git a/galasa-extensions-parent/dev.galasa.cps.rest/src/main/java/dev/galasa/cps/rest/CacheCPS.java b/galasa-extensions-parent/dev.galasa.cps.rest/src/main/java/dev/galasa/cps/rest/CacheCPS.java new file mode 100644 index 00000000..46b47967 --- /dev/null +++ b/galasa-extensions-parent/dev.galasa.cps.rest/src/main/java/dev/galasa/cps/rest/CacheCPS.java @@ -0,0 +1,212 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.cps.rest; + +import java.util.*; +import java.util.Map.Entry; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Null; +import dev.galasa.extensions.common.api.LogFactory; + +import dev.galasa.framework.spi.ConfigurationPropertyStoreException; +import dev.galasa.framework.spi.IConfigurationPropertyStore; + +import org.apache.commons.logging.Log; + +/** + * This class is a CPS implementation that delegates calls to the child CPS it gets passed. + * But it caches responses. + * + * Up-front the implementation reads the entire contents from the CPS and caches it. + * + * Set and Delete of properties are deleted, and the cache state is maintained. + * + * The cache is turned on using the 'framework.cps.rest.cache.is.enabled' property. + * - true : The cacheing is turned on. + * - false : Calls pass directly through to the child CPS implementation. + * Default value: false. + */ +public class CacheCPS implements IConfigurationPropertyStore { + + // The key map key is the fully qualified property name + // The value is the value of the property. + private Map propertyCache ; + + // We use this flag so that we don't try to prime the cache twice. + private boolean isCachePrimed = false ; + + private IConfigurationPropertyStore childCPS ; + + private Log log ; + + private boolean isCacheEnabled = false; + + /** + * The CPS property which this extension draws from to control whether the cache is enabled or not. + */ + public static final String FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED = "framework.cps.rest.cache.is.enabled"; + + + public CacheCPS( IConfigurationPropertyStore childCPS , LogFactory logFactory) throws ConfigurationPropertyStoreException { + + this.log = logFactory.getLog(this.getClass()); + this.propertyCache = new HashMap(); + this.childCPS = childCPS ; + } + + + private synchronized void primeCaches(IConfigurationPropertyStore childCPS ) throws ConfigurationPropertyStoreException { + + // Don't re-prime the caches if they are not primed already. + if (this.isCachePrimed==false) { + + // Only prime the cache once + this.isCachePrimed = true ; + + String isEnabledPropValue = childCPS.getProperty(FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED); + if ((isEnabledPropValue==null)||(isEnabledPropValue.isBlank())) { + log.info("CPS Cache property "+FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED+" not found in child CPS."); + this.isCacheEnabled = false ; + } else { + log.info("CPS Cache property "+FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED+" has a value of "+isEnabledPropValue); + this.isCacheEnabled = Boolean.parseBoolean(isEnabledPropValue); + } + + if (!this.isCacheEnabled) { + log.info("CPS Cache is not enabled..."); + } else { + + log.info("CPS Cache is enabled, and being primed..."); + List namespaces = childCPS.getNamespaces(); + + for( String namespace : namespaces ) { + + if (!namespace.equals("secure")) { + Map propertiesFromNamespace = childCPS.getPropertiesFromNamespace(namespace); + + for( Entry propertyInNamespace : propertiesFromNamespace.entrySet()){ + String propertyValue = propertyInNamespace.getValue(); + String longPropertyName = propertyInNamespace.getKey(); + + propertyCache.put(longPropertyName,propertyValue); + } + } + } + } + log.info("CPS Cache primed with "+Integer.toString(propertyCache.size())+" properties."); + + } + } + + @Override + public List getNamespaces() throws ConfigurationPropertyStoreException { + primeCaches(childCPS); + List results ; + if (isCacheEnabled) { + results = new ArrayList(); + + // Gather a set of the namespaces, so there are no duplicates. + Set namespacesSet = new HashSet<>(); + for (Map.Entry entry : propertyCache.entrySet()){ + String propertyName = entry.getKey(); + String[] parts = propertyName.split("\\."); + if (parts.length > 1) { + String namespace = parts[0]; + namespacesSet.add(namespace); + } + } + + results.addAll(namespacesSet); + } else { + results = this.childCPS.getNamespaces(); + } + return results; + } + + @Override + public @Null String getProperty(@NotNull String fullyQualifiedPropertyName) throws ConfigurationPropertyStoreException { + primeCaches(childCPS); + String result ; + if (isCacheEnabled) { + result = propertyCache.get(fullyQualifiedPropertyName); + } else { + result = this.childCPS.getProperty(fullyQualifiedPropertyName); + } + return result ; + } + + @Override + public void setProperty(@NotNull String key, @NotNull String value) throws ConfigurationPropertyStoreException { + primeCaches(childCPS); + + // Delegate the set of the property to the child CPS + childCPS.setProperty(key, value); + + if (isCacheEnabled) { + // The child changed the property value ok, so we should change the cache version also. + propertyCache.put(key,value); + } + } + + @Override + public @NotNull Map getPrefixedProperties(@NotNull String prefix) + throws ConfigurationPropertyStoreException { + + primeCaches(childCPS); + + Map results; + if (isCacheEnabled) { + results = new HashMap(); + for( Entry property : this.propertyCache.entrySet() ){ + String propName = property.getKey(); + if( propName.startsWith(prefix)) { + String propValue = property.getValue(); + results.put(propName, propValue); + } + } + } else { + results = this.childCPS.getPrefixedProperties(prefix); + } + return results; + } + + @Override + public void deleteProperty(@NotNull String key) throws ConfigurationPropertyStoreException { + + primeCaches(childCPS); + + // Delegate the delete to the underlying CPS. + this.childCPS.deleteProperty(key); + + if (this.isCacheEnabled) { + // Keep our cache in step. + propertyCache.remove(key); + } + } + + @Override + public Map getPropertiesFromNamespace(String namespace) throws ConfigurationPropertyStoreException { + + primeCaches(childCPS); + + Map results ; + if (this.isCacheEnabled) { + results = getPrefixedProperties(namespace); + } else { + results = childCPS.getPropertiesFromNamespace(namespace); + } + return results; + } + + @Override + public void shutdown() throws ConfigurationPropertyStoreException { + + // Delegate this stimulus to the child, to give that a chance of closing resources. + childCPS.shutdown(); + } + +} diff --git a/galasa-extensions-parent/dev.galasa.cps.rest/src/main/java/dev/galasa/cps/rest/RestCPS.java b/galasa-extensions-parent/dev.galasa.cps.rest/src/main/java/dev/galasa/cps/rest/RestCPS.java index fc2ca022..01f0afa4 100644 --- a/galasa-extensions-parent/dev.galasa.cps.rest/src/main/java/dev/galasa/cps/rest/RestCPS.java +++ b/galasa-extensions-parent/dev.galasa.cps.rest/src/main/java/dev/galasa/cps/rest/RestCPS.java @@ -209,7 +209,7 @@ private boolean isPropertyRedacted(String fullyQualifiedPropertyName) { // So we use the /cps/{namespace}/properties?prefix=xxxx so that if the endpoint isn't available, we get 404, // and if the property doesn't exist, then we get null in the map. // Although it's not as efficient on the server-side, performance isn't everything in this case, as local runs - // can be slower/less performant than the ecosyste runs. + // can be slower/less performant than the ecosystem runs. Map properties = getPrefixedProperties(fullyQualifiedPropertyName); propertyValueResult = properties.get(fullyQualifiedPropertyName); } @@ -441,7 +441,7 @@ private Map propertiesToMap(GalasaProperty[] properties) throws C GalasaPropertyData data = property.getData(); String value = data.getValue(); - log.info("galasacps: over rest (with prefix): "+fullyQualifiedPropName+" : "+value); + // log.info("galasacps: over rest (with prefix): "+fullyQualifiedPropName+" : "+value); if (!isPropertyRedacted(fullyQualifiedPropName)) { results.put(fullyQualifiedPropName, value); diff --git a/galasa-extensions-parent/dev.galasa.cps.rest/src/main/java/dev/galasa/cps/rest/RestCPSRegistration.java b/galasa-extensions-parent/dev.galasa.cps.rest/src/main/java/dev/galasa/cps/rest/RestCPSRegistration.java index ff3bb111..500a1ccc 100644 --- a/galasa-extensions-parent/dev.galasa.cps.rest/src/main/java/dev/galasa/cps/rest/RestCPSRegistration.java +++ b/galasa-extensions-parent/dev.galasa.cps.rest/src/main/java/dev/galasa/cps/rest/RestCPSRegistration.java @@ -17,6 +17,7 @@ import dev.galasa.extensions.common.impl.HttpClientFactoryImpl; import dev.galasa.extensions.common.impl.LogFactoryImpl; import dev.galasa.framework.spi.ConfigurationPropertyStoreException; +import dev.galasa.framework.spi.IConfigurationPropertyStore; import dev.galasa.framework.spi.IConfigurationPropertyStoreRegistration; import dev.galasa.framework.spi.IFrameworkInitialisation; @@ -74,13 +75,17 @@ public void initialise(@NotNull IFrameworkInitialisation frameworkInitialisation throw new ConfigurationPropertyStoreException(msg,ex); } + IConfigurationPropertyStore baseCPS = new RestCPS( + ecosystemRestApi, + httpClientFacotory, + jwtProvider, + logFactory + ); + + IConfigurationPropertyStore cacheCPS = new CacheCPS(baseCPS, logFactory); + frameworkInitialisation.registerConfigurationPropertyStore( - new RestCPS( - ecosystemRestApi, - httpClientFacotory, - jwtProvider, - logFactory - ) + cacheCPS ); } } diff --git a/galasa-extensions-parent/dev.galasa.cps.rest/src/test/java/dev/galasa/cps/rest/TestCacheCPS.java b/galasa-extensions-parent/dev.galasa.cps.rest/src/test/java/dev/galasa/cps/rest/TestCacheCPS.java new file mode 100644 index 00000000..61d72d35 --- /dev/null +++ b/galasa-extensions-parent/dev.galasa.cps.rest/src/test/java/dev/galasa/cps/rest/TestCacheCPS.java @@ -0,0 +1,421 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.cps.rest; + +import org.junit.Test; +import static org.assertj.core.api.Assertions.*; + +import dev.galasa.cps.rest.mocks.MockCPS; +import dev.galasa.extensions.mocks.MockLogFactory; +import dev.galasa.extensions.common.api.LogFactory; + +import java.util.*; + +public class TestCacheCPS { + + LogFactory logFactory = new MockLogFactory(); + + @Test + public void testCanCreateNewCache() throws Exception { + // Given... + MockCPS mockCPS = new MockCPS(Map.of( + CacheCPS.FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED,"true", + "framework.prop1","23", + "mynamespace.prop1", "24" + )); + new CacheCPS(mockCPS, logFactory); + } + + @Test + public void testCanGetNamespacesOk() throws Exception { + // Given... + MockCPS mockCPS = new MockCPS(Map.of( + CacheCPS.FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED,"true", + "framework.prop1","23", + "mynamespace.prop1", "24" + )); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + // When... + List namespaces = cache.getNamespaces(); + cache.getNamespaces(); + cache.getNamespaces(); + + // Then... + assertThat(namespaces).contains("framework","mynamespace"); + // We expect the underlying child CPS to have it's namespaces queried once only, when the cache is primed. + assertThat(mockCPS.callCounterForGetNamespaces).isEqualTo(1); + } + + @Test + public void testCanGetNamespacesOkWithCacheDisabled() throws Exception { + // Given... + MockCPS mockCPS = new MockCPS(Map.of( + // Disabled cache... CacheCPS.FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED,"true", + "framework.prop1","23", + "mynamespace.prop1", "24" + )); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + // When... + List namespaces = cache.getNamespaces(); + cache.getNamespaces(); + cache.getNamespaces(); + + // Then... + assertThat(namespaces).contains("framework","mynamespace"); + // We expect the underlying child CPS to have it's namespaces queried once per request above. + assertThat(mockCPS.callCounterForGetNamespaces).isEqualTo(3); + } + + @Test + public void testNamespacesAreCached() throws Exception { + MockCPS mockCPS = new MockCPS(Map.of( + CacheCPS.FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED,"true", + "framework.prop1","23", + "mynamespace.prop1", "24" + )); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + // When... + cache.getNamespaces(); + List namespaces2 = cache.getNamespaces(); + + // Then... + assertThat(namespaces2).contains("framework","mynamespace"); + assertThat(mockCPS.callCounterForGetNamespaces).isEqualTo(1); + } + + + @Test + public void testGettingTwoPropertiesWithCacheEnabledOnlyGetsOnePropertyIndividuallyFromTheChildCPS() throws Exception { + // Given... + MockCPS mockCPS = new MockCPS(Map.of( + CacheCPS.FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED,"true", + "framework.frodo","24", + "mynamespace.bilbo", "36" + )); + + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + // When... + String prop1Value = cache.getProperty("framework.frodo"); + String prop2Value = cache.getProperty("mynamespace.bilbo"); + + // Then... + assertThat(prop1Value).isEqualTo("24"); + assertThat(prop2Value).isEqualTo("36"); + + // The mock should have been called once for the is-cache-enabled property + // Other properties were recovered in bulk. + // The two above getProperty calls should have been dealt with by the cache. + assertThat(mockCPS.callCounterForGetProperty).isEqualTo(1); + } + + + @Test + public void testSecureNamespacePropsAreNotCached() throws Exception { + // Given... + // Even when the underlying CPS has secure properties inside, + MockCPS mockCPS = new MockCPS(Map.of( + CacheCPS.FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED,"true", + "secure.prop1.value","24" + )); + + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + // When... + String prop1Value = cache.getProperty("secure.prop1.value"); + + // Then... + // We can't get those values out though the cache. + assertThat(prop1Value).isNull(); + } + + + @Test + public void testGettingTwoPropertiesWithCacheDisabledGetsThreePropertiesIndividuallyFromTheChildCPS() throws Exception { + // Given... + MockCPS mockCPS = new MockCPS(Map.of( + // CacheCPS.FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED,"true", Caching is disabled. + "framework.frodo","24", + "mynamespace.bilbo", "36" + )); + + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + // When... + String prop1Value = cache.getProperty("framework.frodo"); + String prop2Value = cache.getProperty("mynamespace.bilbo"); + + // Then... + assertThat(prop1Value).isEqualTo("24"); + assertThat(prop2Value).isEqualTo("36"); + + // The mock should have been called once for the is-cache-enabled property + // and the 2 above calls to getProperty should have been delegated to the child CPS. + assertThat(mockCPS.callCounterForGetProperty).isEqualTo(3); + } + + @Test + public void testTryToGetAMissingPropertyToGetNullValueBack() throws Exception { + // Given... + MockCPS mockCPS = new MockCPS(Map.of( + CacheCPS.FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED,"true", + "framework.prop1","23", + "mynamespace.prop1", "24" + )); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + // When... + String propValue = cache.getProperty("framework.doesntexist"); + + // Then... + assertThat(propValue).isNull(); + } + + @Test + public void testTryToGetAMissingPropertyToGetNullValueBackWithCacheDisabled() throws Exception { + // Given... + MockCPS mockCPS = new MockCPS(Map.of( + CacheCPS.FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED,"false", // disabled. + "framework.prop1","23", + "mynamespace.prop1", "24" + )); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + // When... + String propValue = cache.getProperty("framework.doesntexist"); + + // Then... + assertThat(propValue).isNull(); + } + + @Test + public void testSetPropertyDelegatesToClientCPS() throws Exception { + // Given... + MockCPS mockCPS = createFrodoAndBilboMockCPS(true); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + String frodoAge = cache.getProperty("framework.frodo.age"); + assertThat(frodoAge).isEqualTo("24"); + + // When... + cache.setProperty("framework.frodo.age","25"); + + String frodoAgeAfter = cache.getProperty("framework.frodo.age"); + assertThat(frodoAgeAfter).isEqualTo("25"); + } + + @Test + public void testSetPropertyDelegatesToClientCPSWithCacheDisbaled() throws Exception { + // Given... + MockCPS mockCPS = createFrodoAndBilboMockCPS(false); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + String frodoAge = cache.getProperty("framework.frodo.age"); + assertThat(frodoAge).isEqualTo("24"); + + // When... + cache.setProperty("framework.frodo.age","25"); + + String frodoAgeAfter = cache.getProperty("framework.frodo.age"); + assertThat(frodoAgeAfter).isEqualTo("25"); + } + + private MockCPS createFrodoAndBilboMockCPS(boolean isCacheEnabled) { + MockCPS mockCPS = new MockCPS(Map.of( + CacheCPS.FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED,Boolean.toString(isCacheEnabled), + "framework.frodo.age","24", + "framework.frodo.height","95cm", + "mynamespace.bilbo.age", "116", + "mynamespace.bilbo.height", "86cm" + )); + return mockCPS; + } + + @Test + public void testCanGetAPropertyByPrefix() throws Exception { + // Given... + MockCPS mockCPS = createFrodoAndBilboMockCPS(true); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + // When... + Map frodo = cache.getPrefixedProperties("framework.frodo"); + Map bilbo = cache.getPrefixedProperties("mynamespace.bilbo"); + + // Then... + assertThat(frodo.keySet().size()).isEqualTo(2); + assertThat(frodo.keySet()).contains("framework.frodo.age"); + assertThat(frodo.keySet()).contains("framework.frodo.height"); + assertThat(frodo.get("framework.frodo.height")).isEqualTo("95cm"); + assertThat(frodo.get("framework.frodo.age")).isEqualTo("24"); + + assertThat(bilbo.keySet().size()).isEqualTo(2); + assertThat(bilbo.keySet()).contains("mynamespace.bilbo.age"); + assertThat(bilbo.keySet()).contains("mynamespace.bilbo.height"); + assertThat(bilbo.get("mynamespace.bilbo.height")).isEqualTo("86cm"); + assertThat(bilbo.get("mynamespace.bilbo.age")).isEqualTo("116"); + } + + @Test + public void testCanGetAPropertyByPrefixCacheDisabled() throws Exception { + // Given... + MockCPS mockCPS = createFrodoAndBilboMockCPS(false); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + // When... + Map frodo = cache.getPrefixedProperties("framework.frodo"); + Map bilbo = cache.getPrefixedProperties("mynamespace.bilbo"); + + // Then... + assertThat(frodo.keySet().size()).isEqualTo(2); + assertThat(frodo.keySet()).contains("framework.frodo.age"); + assertThat(frodo.keySet()).contains("framework.frodo.height"); + assertThat(frodo.get("framework.frodo.height")).isEqualTo("95cm"); + assertThat(frodo.get("framework.frodo.age")).isEqualTo("24"); + + assertThat(bilbo.keySet().size()).isEqualTo(2); + assertThat(bilbo.keySet()).contains("mynamespace.bilbo.age"); + assertThat(bilbo.keySet()).contains("mynamespace.bilbo.height"); + assertThat(bilbo.get("mynamespace.bilbo.height")).isEqualTo("86cm"); + assertThat(bilbo.get("mynamespace.bilbo.age")).isEqualTo("116"); + } + + @Test + public void testDeletePropertyDelegatesToChildCPS() throws Exception { + + // Given... + MockCPS mockCPS = createFrodoAndBilboMockCPS(true); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + String bilboAgeBefore = cache.getProperty("mynamespace.bilbo.age"); + assertThat(bilboAgeBefore).isNotNull(); + assertThat(mockCPS.callCounterForDeleteProperty).isEqualTo(0); + + // When... + cache.deleteProperty("mynamespace.bilbo.age"); + + // Check that the child CPS was told to delete the property + assertThat(mockCPS.callCounterForDeleteProperty).isEqualTo(1); + + // Check that the cache has been updated also. + String bilboAgeAfter= cache.getProperty("mynamespace.bilbo.age"); + assertThat(bilboAgeAfter).isNull(); + } + + @Test + public void testDeletePropertyDelegatesToChildCPSCacheDisabled() throws Exception { + + // Given... + MockCPS mockCPS = createFrodoAndBilboMockCPS(false); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + String bilboAgeBefore = cache.getProperty("mynamespace.bilbo.age"); + assertThat(bilboAgeBefore).isNotNull(); + assertThat(mockCPS.callCounterForDeleteProperty).isEqualTo(0); + + // When... + cache.deleteProperty("mynamespace.bilbo.age"); + + // Check that the child CPS was told to delete the property + assertThat(mockCPS.callCounterForDeleteProperty).isEqualTo(1); + + // Check that the cache has been updated also. + String bilboAgeAfter= cache.getProperty("mynamespace.bilbo.age"); + assertThat(bilboAgeAfter).isNull(); + } + + @Test + public void testCanGetAllPropertiesFromANamespaceOk() throws Exception { + // Given... + MockCPS mockCPS = createFrodoAndBilboMockCPS(true); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + // When... + Map frodo = cache.getPropertiesFromNamespace("framework"); + cache.getPropertiesFromNamespace("framework"); + cache.getPropertiesFromNamespace("framework"); + + // Then... + // We expect an extra property which controls cache enable/disable. + assertThat(frodo.keySet().size()).isEqualTo(2+1); + assertThat(frodo.keySet()).contains("framework.frodo.age"); + assertThat(frodo.keySet()).contains("framework.frodo.height"); + assertThat(frodo.get("framework.frodo.height")).isEqualTo("95cm"); + assertThat(frodo.get("framework.frodo.age")).isEqualTo("24"); + + // The child call should have been called twice as we prime the cache. + assertThat(mockCPS.callCounterForGetPropertiesFromNamespace).isEqualTo(2); + } + + @Test + public void testCanGetAllPropertiesFromANamespaceOkCacheDisabled() throws Exception { + // Given... + MockCPS mockCPS = createFrodoAndBilboMockCPS(false); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + // When... + Map frodo = cache.getPropertiesFromNamespace("framework"); + cache.getPropertiesFromNamespace("framework"); + cache.getPropertiesFromNamespace("framework"); + + // Then... + // We expect an extra property which controls cache enable/disable. + assertThat(frodo.keySet().size()).isEqualTo(3); + assertThat(frodo.keySet()).contains("framework.frodo.age"); + assertThat(frodo.keySet()).contains("framework.frodo.height"); + assertThat(frodo.get("framework.frodo.height")).isEqualTo("95cm"); + assertThat(frodo.get("framework.frodo.age")).isEqualTo("24"); + + // The child call should have been called three times as above, + // priming the cache will have been skipped with the cache disabled. + assertThat(mockCPS.callCounterForGetPropertiesFromNamespace).isEqualTo(3); + } + + @Test + public void testShutdownShutsDownTheChildCPS() throws Exception { + // Given... + MockCPS mockCPS = createFrodoAndBilboMockCPS(true); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + assertThat(mockCPS.callCounterForShutdown).isEqualTo(0); + + cache.shutdown(); + + assertThat(mockCPS.callCounterForShutdown).isEqualTo(1); + } + + @Test + public void testShutdownShutsDownTheChildCPSCacheDisabled() throws Exception { + // Given... + MockCPS mockCPS = createFrodoAndBilboMockCPS(false); + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + assertThat(mockCPS.callCounterForShutdown).isEqualTo(0); + + cache.shutdown(); + + assertThat(mockCPS.callCounterForShutdown).isEqualTo(1); + } + + @Test + public void testSettingAPropertyCanCreateANewNamespaceInCache() throws Exception { + // Given... + MockCPS mockCPS = new MockCPS(Map.of( + CacheCPS.FEATURE_FLAG_CPS_PROP_CACHED_CPS_ENABLED,"true" + )); + + CacheCPS cache = new CacheCPS(mockCPS, logFactory); + + // When... + cache.setProperty("mynamespace.frodo.age","23"); + List namespaces = cache.getNamespaces(); + + // Then... + assertThat(namespaces).hasSize(2).contains("framework","mynamespace"); + } +} diff --git a/galasa-extensions-parent/dev.galasa.cps.rest/src/test/java/dev/galasa/cps/rest/TestRestCPSRegistration.java b/galasa-extensions-parent/dev.galasa.cps.rest/src/test/java/dev/galasa/cps/rest/TestRestCPSRegistration.java index 7ab863bc..42cedd4c 100644 --- a/galasa-extensions-parent/dev.galasa.cps.rest/src/test/java/dev/galasa/cps/rest/TestRestCPSRegistration.java +++ b/galasa-extensions-parent/dev.galasa.cps.rest/src/test/java/dev/galasa/cps/rest/TestRestCPSRegistration.java @@ -46,7 +46,7 @@ public void TestCanInitialiseARegistrationOK() throws Exception { List stores = mockFrameworkInit.getRegisteredConfigurationPropertyStores(); assertThat(stores).isNotNull().hasSize(1); - assertThat(stores.get(0)).isInstanceOf(RestCPS.class); + assertThat(stores.get(0)).isInstanceOf(CacheCPS.class); } @Test diff --git a/galasa-extensions-parent/dev.galasa.cps.rest/src/test/java/dev/galasa/cps/rest/mocks/MockCPS.java b/galasa-extensions-parent/dev.galasa.cps.rest/src/test/java/dev/galasa/cps/rest/mocks/MockCPS.java new file mode 100644 index 00000000..2632d6bb --- /dev/null +++ b/galasa-extensions-parent/dev.galasa.cps.rest/src/test/java/dev/galasa/cps/rest/mocks/MockCPS.java @@ -0,0 +1,133 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package dev.galasa.cps.rest.mocks; + +import java.util.*; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Null; + +import dev.galasa.framework.spi.ConfigurationPropertyStoreException; +import dev.galasa.framework.spi.IConfigurationPropertyStore; + + +/** + * A CPS implementation which holds a hashmap as the basis of it's property store. + * + * It sits in memory and leaves no debris, so is suitable for use with unit tests. + */ +public class MockCPS implements IConfigurationPropertyStore { + + public MockCPS() { + this(new HashMap()); + } + + public MockCPS(Map properties) { + // Take a clone of the in-coming properties so we have a mutable map. + this.properties = new HashMap<>(properties); + + // Check that the test isn't adding an invalid property. + for( Map.Entry property : properties.entrySet()) { + String propName = property.getKey(); + String[] parts = propName.split("\\."); + if (parts.length < 2) { + throw new RuntimeException("Test failed. The test data could not be loaded into the mockCPS. Property '"+propName+"' should have a '.' character inside."); + } + } + } + + public int callCounterForGetNamespaces = 0 ; + public int callCounterForGetPropertiesFromNamespace = 0 ; + public int callCounterForShutdown = 0 ; + public int callCounterForSetProperty = 0 ; + public int callCounterForDeleteProperty = 0 ; + public int callCounterForGetProperty = 0; + + public Map properties ; + + @Override + public Map getPropertiesFromNamespace(String namespace) throws ConfigurationPropertyStoreException { + + Map results = new HashMap(); + + for( Map.Entry property : properties.entrySet()) { + String propName = property.getKey(); + String propValue = property.getValue(); + + String propNamespace = propName.split("\\.")[0]; + + if (propNamespace.equals(namespace)) { + results.put(propName,propValue); + } + } + + callCounterForGetPropertiesFromNamespace+=1; + + return results; + } + + @Override + public List getNamespaces() throws ConfigurationPropertyStoreException { + callCounterForGetNamespaces +=1; + + List namespaces = new ArrayList<>(); + + // Gather all the unique namespaces by looking at the property keys. + Set namespacesSet = new HashSet<>(); + for( Map.Entry property : properties.entrySet()) { + String propName = property.getKey(); + String propNamespace = propName.split("\\.")[0]; + + namespacesSet.add(propNamespace); + } + + namespaces.addAll(namespacesSet); + + return namespaces; + } + + @Override + public @Null String getProperty(@NotNull String key) throws ConfigurationPropertyStoreException { + + callCounterForGetProperty +=1 ; + return properties.get(key); + } + + @Override + public @NotNull Map getPrefixedProperties(@NotNull String prefix) + throws ConfigurationPropertyStoreException { + + Map prefixedProps = new HashMap<>(); + for( Map.Entry property : properties.entrySet()) { + String propName = property.getKey(); + String propValue = property.getValue(); + if (propName.startsWith(prefix)) { + prefixedProps.put(propName,propValue); + } + } + return prefixedProps; + } + + @Override + public void setProperty(@NotNull String key, @NotNull String value) throws ConfigurationPropertyStoreException { + callCounterForSetProperty +=1 ; + + properties.put(key,value); + } + + @Override + public void deleteProperty(@NotNull String key) throws ConfigurationPropertyStoreException { + callCounterForDeleteProperty += 1; + + this.properties.remove(key); + } + + @Override + public void shutdown() throws ConfigurationPropertyStoreException { + callCounterForShutdown +=1; + } + +} diff --git a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/Errors.java b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/Errors.java index 38dfa21d..6470c571 100644 --- a/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/Errors.java +++ b/galasa-extensions-parent/dev.galasa.extensions.common/src/main/java/dev/galasa/extensions/common/Errors.java @@ -57,6 +57,7 @@ public enum Errors { ERROR_GALASA_REST_CALL_TO_GET_ALL_CPS_NAMESPACES_NON_OK_STATUS (7019,"GAL7019E: Could not get the namespace information from URL ''{0}''. Status code ''{1}'' is not 200."), ERROR_GALASA_REST_CALL_TO_GET_CPS_NAMESPACES_FAILED (7020,"GAL7020E: Could not get the CPS namespaces information from URL ''{0}''. Cause: {1}"), ERROR_GALASA_REST_CALL_TO_GET_CPS_NAMESPACES_BAD_JSON_RETURNED (7021,"GAL7021E: Could not get the CPS namespaces value from URL ''{0}''. Cause: Bad json returned from the server. {1}"), + ; private String template;