diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/CacheEntry.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/CacheEntry.cls
new file mode 100644
index 00000000000..833eab8c9e5
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/CacheEntry.cls
@@ -0,0 +1,49 @@
+/**
+ * Represents an entry that exists in the cache.
+ *
+ * Is dated based on the creation time so that entries can later be checked for their staleness
+ *
+ * @group Cache
+ */
+public inherited sharing class CacheEntry
+{
+ Long createdOnEpoch;
+ Object value;
+
+ private Long ageInSeconds
+ {
+ get
+ {
+ return ortoo_DateLiterals.epochTime - this.createdOnEpoch;
+ }
+ }
+
+ public CacheEntry( Object value )
+ {
+ this.value = value;
+ this.createdOnEpoch = ortoo_DateLiterals.epochTime;
+ }
+
+ /**
+ * States if the current entry is younger than or equal to the the stated age in seconds
+ *
+ * @param Long The age to compare against, in seconds
+ * @return Boolean Is the current entry younger or equal to the given age
+ */
+ public Boolean isYoungerThanOrEqualTo( Long compareAgeInSeconds )
+ {
+ Contract.requires( compareAgeInSeconds != null, 'isYoungerThanOrEqualTo called with a null compareAgeInSeconds' );
+
+ return ageInSeconds <= compareAgeInSeconds;
+ }
+
+ /**
+ * Returns the current value of the entry
+ *
+ * @return Object The value of the entry
+ */
+ public Object getValue()
+ {
+ return this.value;
+ }
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/CacheEntry.cls-meta.xml b/framework/default/ortoo-core/default/classes/fflib-extension/caching/CacheEntry.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/CacheEntry.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/CachedSoqlExecutor.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/CachedSoqlExecutor.cls
index 64b3cdd4978..e925c82b05f 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/CachedSoqlExecutor.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/CachedSoqlExecutor.cls
@@ -5,6 +5,8 @@
*
* If used, it is recommended that triggers are added to those objects, or code added to the UI that updates the objects
* referenced in the SOQL that invalidate the cache.
+ *
+ * @group Cache
*/
public inherited sharing class CachedSoqlExecutor //NOPMD: incorrect report of too many public methods caused by inner classes
{
@@ -54,6 +56,48 @@ public inherited sharing class CachedSoqlExecutor //NOPMD: incorrect report of t
* @return List The records that match
*/
public List query( String soql )
+ {
+ return query( soql, new SimpleCacheRetriever( cacheWrapper ) );
+ }
+
+ /**
+ * Perform the given query, first checking if the Org Platform Cache Partition contains results for that SOQL.
+ * If so, the cached versions are returned.
+ * If not, the query is executed against the database and the result cached.
+ * If, for any reason, a cache read or write cannot be performed, the method will continue without an exception.
+ * Errors can be seen in the configured log destination (uses LoggerService)
+ *
+ * @param String The SOQL to return the results for
+ * @param Long The maximum age that the retrieved entry is allowed to be before a cache miss is recorded
+ * @return List The records that match
+ */
+ public List query( String soql, Long maximumAgeInSeconds )
+ {
+ Contract.requires( maximumAgeInSeconds != null, 'query called with a null maximumAgeInSeconds' );
+
+ return query( soql, new MaximumAgeCacheRetriever( cacheWrapper, maximumAgeInSeconds ) );
+ }
+
+ /**
+ * Perform the given query, first checking if the Org Platform Cache Partition contains results for that SOQL.
+ * If so, the cached versions are returned.
+ * If not, the query is executed against the database and the result cached.
+ * If, for any reason, a cache read or write cannot be performed, the method will continue without an exception.
+ * Errors can be seen in the configured log destination (uses LoggerService)
+ *
+ * @param String The SOQL to return the results for
+ * @param DateTime The minimum (most recent) datetime that the retrieved entry is allowed to have been created on
+ * before it is registered as a cache miss
+ * @return List The records that match
+ */
+ public List query( String soql, DateTime minimumDateTimeAdded )
+ {
+ Contract.requires( minimumDateTimeAdded != null, 'query called with a null minimumDateTimeAdded' );
+
+ return query( soql, new MinimimDateAddedCacheRetriever( cacheWrapper, minimumDateTimeAdded ) );
+ }
+
+ private List query( String soql, ICacheRetriever cacheRetriever )
{
Contract.requires( soql != null, 'query called with a null soql' );
@@ -64,7 +108,7 @@ public inherited sharing class CachedSoqlExecutor //NOPMD: incorrect report of t
{
if ( cacheWrapper.hasAccessToCache() )
{
- returnValues = (List)cacheWrapper.get( key );
+ returnValues = (List)cacheRetriever.get( key );
}
else
{
@@ -121,4 +165,57 @@ public inherited sharing class CachedSoqlExecutor //NOPMD: incorrect report of t
{
return EncodingUtil.convertToHex( Crypto.generateDigest( 'SHA1', Blob.valueOf( soql ) ) );
}
+
+ private interface ICacheRetriever
+ {
+ List get( String key );
+ }
+
+ private inherited sharing class SimpleCacheRetriever implements ICacheRetriever
+ {
+ ICacheAdaptor cacheWrapper;
+
+ private SimpleCacheRetriever( ICacheAdaptor cacheWrapper )
+ {
+ this.cacheWrapper = cacheWrapper;
+ }
+
+ public List get( String key )
+ {
+ return (List)cacheWrapper.get( key );
+ }
+ }
+
+ private inherited sharing class MaximumAgeCacheRetriever implements ICacheRetriever
+ {
+ ICacheAdaptor cacheWrapper;
+ Long maximumAge;
+
+ private MaximumAgeCacheRetriever( ICacheAdaptor cacheWrapper, Long maximumAge )
+ {
+ this.cacheWrapper = cacheWrapper;
+ this.maximumAge = maximumAge;
+ }
+
+ public List get( String key )
+ {
+ return (List)cacheWrapper.get( key, maximumAge );
+ }
+ }
+
+ private inherited sharing class MinimimDateAddedCacheRetriever implements ICacheRetriever
+ {
+ ICacheAdaptor cacheWrapper;
+ DateTime minimumDateAdded;
+ private MinimimDateAddedCacheRetriever( ICacheAdaptor cacheWrapper, DateTime minimumDateAdded )
+ {
+ this.cacheWrapper = cacheWrapper;
+ this.minimumDateAdded = minimumDateAdded;
+ }
+
+ public List get( String key )
+ {
+ return (List)cacheWrapper.get( key, minimumDateAdded );
+ }
+ }
}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/ICacheAdaptor.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/ICacheAdaptor.cls
index a145dcd70c1..055442c9438 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/ICacheAdaptor.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/ICacheAdaptor.cls
@@ -6,12 +6,16 @@
* * Cache.Session / Cache.SessionPartition
*
* This allows for the types of cache to be used interchangably / and dynamically
+ *
+ * @group Cache
*/
public interface ICacheAdaptor
{
Boolean hasAccessToCache();
Boolean isACache();
Object get( String key );
+ Object get( String key, DateTime minimumDateTime );
+ Object get( String key, Long maximumAgeInSeconds );
void put( String key, Object value, Integer lifespan );
Boolean contains( String key );
void remove( String key );
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/NullCache.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/NullCache.cls
index 7e3d189cc44..0bbe17a100e 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/NullCache.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/NullCache.cls
@@ -4,6 +4,8 @@
* Allows for the use of code that will automatically interact with a cache and be able to switch that off dynamically and simply.
*
* All users are assumed to be allowed to use the cache, but it describes itself as 'not a cache' and effectively does nothing.
+ *
+ * @group Cache
*/
public inherited sharing class NullCache implements ICacheAdaptor
{
@@ -22,6 +24,16 @@ public inherited sharing class NullCache implements ICacheAdaptor
return null;
}
+ public Object get( String key, DateTime minimumDateTime )
+ {
+ return null;
+ }
+
+ public Object get( String key, Long maximumAgeInSeconds )
+ {
+ return null;
+ }
+
public Set getKeys()
{
return new Set();
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/ObjectCache.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/ObjectCache.cls
index 9be2b90d047..f71160d7125 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/ObjectCache.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/ObjectCache.cls
@@ -7,6 +7,11 @@
*
* Note: The lifespan of a given object is reset whenever any object in that object's list is written.
* That is, either the whole of the list is aged out, or none of it is.
+ *
+ * Note: This implementation does not include age based stalesness on retrieval, but could be enhanced to, if required.
+ * See the alternative 'get' options that are available on ICacheAdaptor
+ *
+ * @group Cache
*/
public inherited sharing class ObjectCache
{
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/OrgCache.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/OrgCache.cls
index 8e34d31ba22..a8a819d4fab 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/OrgCache.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/OrgCache.cls
@@ -5,6 +5,8 @@
* custom permission as defined in 'CAN_ACCESS_CACHE_PERMISSION'.
*
* Attempting to access the cache without this permission will result in an OrgCache.AccessViolationException
+ *
+ * @group Cache
*/
public inherited sharing class OrgCache implements ICacheAdaptor
{
@@ -50,12 +52,46 @@ public inherited sharing class OrgCache implements ICacheAdaptor
* Retrieve the cache entry with the given key.
*
* If the user does not have access to the cache, will throw an AccessViolationException.
- * If the entry does not exist in the cache, will return null
+ * If the entry does not exist in the cache (I.E. is a "cache miss"), will return null
*
* @param String The key for the object to retrieve
* @return Object The cached object, if it exists
*/
public Object get( String key )
+ {
+ Long maximumAgeInSeconds = null;
+ return get( key, maximumAgeInSeconds );
+ }
+
+ /**
+ * Retrieve the cache entry with the given key, but only if it was created after the stated date time
+ *
+ * If the user does not have access to the cache, will throw an AccessViolationException.
+ * If the entry does not exist in the cache (I.E. is a "cache miss"), will return null
+ *
+ * @param String The key for the object to retrieve
+ * @return Object The cached object, if it exists
+ */
+ public Object get( String key, DateTime minimumDateTime )
+ {
+ Long maximumAgeInSeconds;
+ if ( minimumDateTime != null )
+ {
+ maximumAgeInSeconds = ortoo_DateLiterals.epochTime - DateTimeUtils.convertToEpochTime( minimumDateTime );
+ }
+ return get( key, maximumAgeInSeconds );
+ }
+
+ /**
+ * Retrieve the cache entry with the given key, but only if it is younger than the stated number of seconds
+ *
+ * If the user does not have access to the cache, will throw an AccessViolationException.
+ * If the entry does not exist in the cache (I.E. is a "cache miss"), will return null
+ *
+ * @param String The key for the object to retrieve
+ * @return Object The cached object, if it exists
+ */
+ public Object get( String key, Long maximumAgeInSeconds )
{
Contract.requires( String.isNotBlank( key ), 'get called with a blank key' );
@@ -64,10 +100,27 @@ public inherited sharing class OrgCache implements ICacheAdaptor
throw new AccessViolationException( Label.ortoo_core_org_cache_access_violation )
.setErrorCode( FrameworkErrorCodes.CACHE_ACCESS_VIOLATION )
.addContext( 'method', 'get' )
- .addContext( 'key', key );
+ .addContext( 'key', key )
+ .addContext( 'maximumAgeInSeconds', maximumAgeInSeconds );
}
- return Cache.Org.get( createFullyQualifiedKey( key ) );
+ Object rawCacheValue = Cache.Org.get( createFullyQualifiedKey( key ) );
+
+ if ( rawCacheValue instanceOf CacheEntry )
+ {
+ CacheEntry cacheValue = (CacheEntry)rawCacheValue;
+ if ( maximumAgeInSeconds == null || cacheValue.isYoungerThanOrEqualTo( maximumAgeInSeconds ) )
+ {
+ return cacheValue.getValue();
+ }
+ }
+
+ if ( maximumAgeInSeconds == null ) // if we don't care about the age, then return it
+ {
+ return rawCacheValue;
+ }
+
+ return null; // if there is no age, or it is too old, regard it as a cache miss
}
/**
@@ -114,7 +167,9 @@ public inherited sharing class OrgCache implements ICacheAdaptor
.addContext( 'value', value );
}
- Cache.Org.put( createFullyQualifiedKey( key ), value, lifespan, Cache.Visibility.NAMESPACE, true ); // immutable outside of namespace
+ CacheEntry cacheEntry = new CacheEntry( value );
+
+ Cache.Org.put( createFullyQualifiedKey( key ), cacheEntry, lifespan, Cache.Visibility.NAMESPACE, true ); // immutable outside of namespace
}
/**
@@ -187,6 +242,7 @@ public inherited sharing class OrgCache implements ICacheAdaptor
return Cache.OrgPartition.createFullyQualifiedPartition( PackageUtils.NAMESPACE_PREFIX, PARTITION_NAME );
}
+ @testVisible
private String createFullyQualifiedKey( String key )
{
return Cache.OrgPartition.createFullyQualifiedKey( PackageUtils.NAMESPACE_PREFIX, PARTITION_NAME, key );
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/SessionCache.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/SessionCache.cls
index 38b4f7f7cfe..443b414c998 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/SessionCache.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/SessionCache.cls
@@ -2,6 +2,8 @@
* An implementation of the ICacheAdaptor that utilises the Session Level Platform Cache.
*
* All users are assumed to have access to the cache.
+ *
+ * @group Cache
*/
public inherited sharing class SessionCache implements ICacheAdaptor
{
@@ -34,9 +36,58 @@ public inherited sharing class SessionCache implements ICacheAdaptor
* @return Object The cached object, if it exists
*/
public Object get( String key )
+ {
+ Long maximumAgeInSeconds = null;
+ return get( key, maximumAgeInSeconds );
+ }
+
+ /**
+ * Retrieve the cache entry with the given key, but only if it was created after the stated date time
+ *
+ * If the entry does not exist in the cache (I.E. is a "cache miss"), will return null
+ *
+ * @param String The key for the object to retrieve
+ * @return Object The cached object, if it exists
+ */
+ public Object get( String key, DateTime minimumDateTime )
+ {
+ Long maximumAgeInSeconds;
+ if ( minimumDateTime != null )
+ {
+ maximumAgeInSeconds = ortoo_DateLiterals.epochTime - DateTimeUtils.convertToEpochTime( minimumDateTime );
+ }
+ return get( key, maximumAgeInSeconds );
+ }
+
+ /**
+ * Retrieve the cache entry with the given key, but only if it is younger than the stated number of seconds
+ *
+ * If the entry does not exist in the cache (I.E. is a "cache miss"), will return null
+ *
+ * @param String The key for the object to retrieve
+ * @return Object The cached object, if it exists
+ */
+ public Object get( String key, Long maximumAgeInSeconds )
{
Contract.requires( String.isNotBlank( key ), 'get called with a blank key' );
- return Cache.Session.get( key );
+
+ Object rawCacheValue = Cache.Session.get( createFullyQualifiedKey( key ) );
+
+ if ( rawCacheValue instanceOf CacheEntry )
+ {
+ CacheEntry cacheValue = (CacheEntry)rawCacheValue;
+ if ( maximumAgeInSeconds == null || cacheValue.isYoungerThanOrEqualTo( maximumAgeInSeconds ) )
+ {
+ return cacheValue.getValue();
+ }
+ }
+
+ if ( maximumAgeInSeconds == null ) // if we don't care about the age, then return it
+ {
+ return rawCacheValue;
+ }
+
+ return null; // if there is no age, or it is too old, regard it as a cache miss
}
/**
@@ -65,7 +116,9 @@ public inherited sharing class SessionCache implements ICacheAdaptor
Contract.requires( lifespan >= 0, 'put called with a negative lifespan' );
// Note that the maximum is handled by Salesforce, just in case it increases
- Cache.Session.put( key, value, lifespan, Cache.Visibility.NAMESPACE, true ); // immutable outside of namespace
+ CacheEntry cacheEntry = new CacheEntry( value );
+
+ Cache.Session.put( key, cacheEntry, lifespan, Cache.Visibility.NAMESPACE, true ); // immutable outside of namespace
}
/**
@@ -111,6 +164,7 @@ public inherited sharing class SessionCache implements ICacheAdaptor
return Cache.SessionPartition.createFullyQualifiedPartition( PackageUtils.NAMESPACE_PREFIX, PARTITION_NAME );
}
+ @testVisible
private String createFullyQualifiedKey( String key )
{
return Cache.SessionPartition.createFullyQualifiedKey( PackageUtils.NAMESPACE_PREFIX, PARTITION_NAME, key );
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/CacheEntryTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/CacheEntryTest.cls
new file mode 100644
index 00000000000..972a626d411
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/CacheEntryTest.cls
@@ -0,0 +1,78 @@
+@isTest
+private without sharing class CacheEntryTest
+{
+ @isTest
+ private static void getValue_whenCalled_returnsTheValue() // NOPMD: Test method name format
+ {
+ Object expected = 'thevalue';
+ CacheEntry entry = new CacheEntry( expected );
+
+ Test.startTest();
+ Object got = entry.getValue();
+ Test.stopTest();
+
+ System.assertEquals( expected, got, 'getValue, when called, will return the value' );
+ }
+
+ @isTest
+ private static void isYoungerThanOrEqualTo_whenTheCacheIsYounger_returnsTrue() // NOPMD: Test method name format
+ {
+ TestDateTimeUtils.setCurrentDateTime( 0 );
+ CacheEntry entry = new CacheEntry( 'TheCachedEntry' );
+
+ Test.startTest();
+ TestDateTimeUtils.addToCurrentTime( 100 ); // this means the cache entry is 100 seconds old
+ Boolean got = entry.isYoungerThanOrEqualTo( 150 );
+ Test.stopTest();
+
+ System.assert( got, 'isYoungerThanOrEqualTo, when the cache entry is younger, will return true' );
+ }
+
+ @isTest
+ private static void isYoungerThanOrEqualTo_whenTheCacheIsExactlyTheGivenAge_returnsTrue() // NOPMD: Test method name format
+ {
+ TestDateTimeUtils.setCurrentDateTime( 0 );
+ CacheEntry entry = new CacheEntry( 'TheCachedEntry' );
+
+ Test.startTest();
+ TestDateTimeUtils.addToCurrentTime( 100 ); // this means the cache entry is 100 seconds old
+ Boolean got = entry.isYoungerThanOrEqualTo( 100 );
+ Test.stopTest();
+
+ System.assert( got, 'isYoungerThanOrEqualTo, when the cache entry is exactly the stated age, will return true' );
+ }
+
+ @isTest
+ private static void isYoungerThanOrEqualTo_whenTheCacheIsOlder_returnsFalse() // NOPMD: Test method name format
+ {
+ TestDateTimeUtils.setCurrentDateTime( 0 );
+ CacheEntry entry = new CacheEntry( 'TheCachedEntry' );
+
+ Test.startTest();
+ TestDateTimeUtils.addToCurrentTime( 100 ); // this means the cache entry is 100 seconds old
+ Boolean got = entry.isYoungerThanOrEqualTo( 50 );
+ Test.stopTest();
+
+ System.assert( ! got, 'isYoungerThanOrEqualTo, when the cache entry is older, will return false' );
+ }
+
+ @isTest
+ private static void isYoungerThanOrEqualTo_whenPassedANullAge_throwsAnException() // NOPMD: Test method name format
+ {
+ CacheEntry entry = new CacheEntry( 'TheCachedEntry' );
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ entry.isYoungerThanOrEqualTo( null );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ ortoo_Asserts.assertContains( 'isYoungerThanOrEqualTo called with a null compareAgeInSeconds', exceptionMessage, 'isYoungerThanOrEqualTo, when passed a null age, will throw an exception' );
+ }
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/CacheEntryTest.cls-meta.xml b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/CacheEntryTest.cls-meta.xml
new file mode 100644
index 00000000000..dd61d1f917e
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/CacheEntryTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 52.0
+ Active
+
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/CachedSoqlExecutorTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/CachedSoqlExecutorTest.cls
index c54a02baf72..9739eef2de1 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/CachedSoqlExecutorTest.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/CachedSoqlExecutorTest.cls
@@ -497,6 +497,189 @@ private without sharing class CachedSoqlExecutorTest
logger.assertNumberOfLogCalls( 0, 'clearCacheFor against a none cache, when called by a user that does not have access to the org cache, will not create a log' );
}
+ @isTest
+ private static void query_maxAge_session_whenCalledTwiceWithinAge_onlyIssuesOneSoql() // NOPMD: Test method name format
+ {
+ TestLoggerService logger = TestLoggerUtils.registerTestLoggerService();
+
+ String soqlStatement = 'SELECT Id FROM Account';
+
+ setupAccessToSoqlCache( true );
+
+ CachedSoqlExecutor executor = new CachedSoqlExecutor().setScope( CachedSoqlExecutor.CacheScope.SESSION );
+
+ Test.startTest();
+ TestDateTimeUtils.setCurrentDateTime( 0 );
+ List originalResults = executor.query( soqlStatement, 100 );
+
+ TestDateTimeUtils.addToCurrentTime( 50 ); // it is now 50 seconds later
+ logger.clearLogHistory();
+ List secondResults = executor.query( soqlStatement, 100 ); // so this should still return the results
+ Integer soqlCalls = Limits.getQueries();
+ Test.stopTest();
+
+ System.assertEquals( 1, soqlCalls, 'query with a maximum age on session cache, when called twice within the required age, will only issue one SOQL statement' );
+ System.assertEquals( originalResults, secondResults, 'query with a maximum age on session cache, when called twice within the required age, returns the same results in both calls' );
+
+ logger.assertNumberOfLogCalls( 0, 'query with a maximum age on session cache,when called twice within the required age, does not generate a log entry' );
+ }
+
+ @isTest
+ private static void query_maxAge_session_whenCalledTwiceOutsideAge_issuesTwoSoql() // NOPMD: Test method name format
+ {
+ TestLoggerService logger = TestLoggerUtils.registerTestLoggerService();
+
+ String soqlStatement = 'SELECT Id FROM Account';
+
+ setupAccessToSoqlCache( true );
+
+ CachedSoqlExecutor executor = new CachedSoqlExecutor().setScope( CachedSoqlExecutor.CacheScope.SESSION );
+
+ Test.startTest();
+ TestDateTimeUtils.setCurrentDateTime( 0 );
+ List originalResults = executor.query( soqlStatement, 100 );
+
+ TestDateTimeUtils.addToCurrentTime( 100 ); // it is now 50 seconds later
+ logger.clearLogHistory();
+ List secondResults = executor.query( soqlStatement, 50 ); // so this should result in a new query because the previous results are too old
+ Integer soqlCalls = Limits.getQueries();
+ Test.stopTest();
+
+ System.assertEquals( 2, soqlCalls, 'query with a maximum age on session cache, when called twice with the second outside the required age, will issue two SOQL statements' );
+ }
+
+ @isTest
+ private static void query_maxAge_session_whenAgeMeansAQueryReruns_willPutBackIntoCache() // NOPMD: Test method name format
+ {
+ TestLoggerService logger = TestLoggerUtils.registerTestLoggerService();
+
+ String soqlStatement = 'SELECT Id FROM Account';
+
+ setupAccessToSoqlCache( true );
+
+ CachedSoqlExecutor executor = new CachedSoqlExecutor().setScope( CachedSoqlExecutor.CacheScope.SESSION );
+
+ Test.startTest();
+ TestDateTimeUtils.setCurrentDateTime( 0 );
+ executor.query( soqlStatement, 100 );
+
+ TestDateTimeUtils.addToCurrentTime( 100 ); // it is now 100 seconds later
+ executor.query( soqlStatement, 50 ); // so this should result in a new query
+
+ TestDateTimeUtils.addToCurrentTime( 100 ); // it is now another 100 seconds later
+ executor.query( soqlStatement, 150 ); // this should get the results from the second query
+
+ Integer soqlCalls = Limits.getQueries();
+ Test.stopTest();
+
+ System.assertEquals( 2, soqlCalls, 'query with a maximum age on session cache, when the call results in ageing results out, the new results will be cached' );
+ }
+
+ @isTest
+ private static void query_whenPassedANullMaxAge_throwsAnException() // NOPMD: Test method name format
+ {
+ String soqlStatement = 'SELECT Id FROM Account';
+ CachedSoqlExecutor executor = new CachedSoqlExecutor();
+
+ Long nullAge;
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ executor.query( soqlStatement, nullAge );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ ortoo_Asserts.assertContains( 'query called with a null maximumAgeInSeconds', exceptionMessage, 'query, when passed a null maximumAgeInSeconds, will throw an exception' );
+ }
+
+ @isTest
+ private static void query_minDate_session_whenCalledTwiceWithinDate_onlyIssuesOneSoql() // NOPMD: Test method name format
+ {
+ TestLoggerService logger = TestLoggerUtils.registerTestLoggerService();
+
+ DateTime earliestDate = DateTime.newInstance( 2020, 2, 4, 14, 00, 00 );
+ DateTime middleDate = DateTime.newInstance( 2020, 2, 5, 14, 00, 00 );
+ DateTime latestDate = DateTime.newInstance( 2020, 2, 6, 14, 00, 00 );
+
+ String soqlStatement = 'SELECT Id FROM Account';
+
+ setupAccessToSoqlCache( true );
+
+ CachedSoqlExecutor executor = new CachedSoqlExecutor().setScope( CachedSoqlExecutor.CacheScope.SESSION );
+
+ Test.startTest();
+ TestDateTimeUtils.setCurrentDateTime( middleDate );
+ List originalResults = executor.query( soqlStatement, middleDate );
+
+ TestDateTimeUtils.setCurrentDateTime( latestDate );
+ logger.clearLogHistory();
+ List secondResults = executor.query( soqlStatement, earliestDate ); // this date is earlier than the first query ran, so the cache should be used
+ Integer soqlCalls = Limits.getQueries();
+ Test.stopTest();
+
+ System.assertEquals( 1, soqlCalls, 'query with a minimum date on session cache, when called twice within the required age, will only issue one SOQL statement' );
+ System.assertEquals( originalResults, secondResults, 'query with a minimum date on session cache, when called twice within the required age, returns the same results in both calls' );
+
+ logger.assertNumberOfLogCalls( 0, 'query with a minimum date on session cache,when called twice within the required age, does not generate a log entry' );
+ }
+
+ @isTest
+ private static void query_minDate_session_whenCalledTwiceOutsideDate_issuesTwoSoql() // NOPMD: Test method name format
+ {
+ TestLoggerService logger = TestLoggerUtils.registerTestLoggerService();
+
+ DateTime earliestDate = DateTime.newInstance( 2020, 2, 4, 14, 00, 00 );
+ DateTime middleDate = DateTime.newInstance( 2020, 2, 5, 14, 00, 00 );
+ DateTime latestDate = DateTime.newInstance( 2020, 2, 6, 14, 00, 00 );
+
+ String soqlStatement = 'SELECT Id FROM Account';
+
+ setupAccessToSoqlCache( true );
+
+ CachedSoqlExecutor executor = new CachedSoqlExecutor().setScope( CachedSoqlExecutor.CacheScope.SESSION );
+
+ Test.startTest();
+ TestDateTimeUtils.setCurrentDateTime( earliestDate );
+ List originalResults = executor.query( soqlStatement, earliestDate );
+
+ TestDateTimeUtils.setCurrentDateTime( latestDate );
+ logger.clearLogHistory();
+ List secondResults = executor.query( soqlStatement, middleDate ); // this date is later than the first query ran, so the cache should not be used
+ Integer soqlCalls = Limits.getQueries();
+ Test.stopTest();
+
+ System.assertEquals( 2, soqlCalls, 'query with a maximum age on session cache, when called twice with the second outside the required age, will issue two SOQL statements' );
+ }
+
+ @isTest
+ private static void query_minDate_whenPassedANullMinDate_throwsAnException() // NOPMD: Test method name format
+ {
+ String soqlStatement = 'SELECT Id FROM Account';
+ CachedSoqlExecutor executor = new CachedSoqlExecutor();
+
+ DateTime nullDateTime;
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ executor.query( soqlStatement, nullDateTime );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ ortoo_Asserts.assertContains( 'query called with a null minimumDateTimeAdded', exceptionMessage, 'query, when passed a null minimumDateTimeAdded, will throw an exception' );
+ }
+
@isTest
private static void query_none_whenRanFor100Queries_willNotThrow() // NOPMD: Test method name format
{
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/NullCacheTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/NullCacheTest.cls
index e1edc93de90..b2f4ed0dc5c 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/NullCacheTest.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/NullCacheTest.cls
@@ -1,6 +1,9 @@
@isTest
private without sharing class NullCacheTest
{
+ private final static Long NULL_AGE = null;
+ private final static DateTime NULL_MIN_DATE_TIME = null;
+
@isTest
private static void hasAccessToCache_whenCalled_returnsTrue() // NOPMD: Test method name format
{
@@ -25,6 +28,22 @@ private without sharing class NullCacheTest
System.assertEquals( null, got, 'get, when called, will return null' );
}
+ @isTest
+ private static void get_maxAge_whenCalled_returnsNull() // NOPMD: Test method name format
+ {
+ NullCache cache = new NullCache();
+ Object got = cache.get( null, NULL_AGE );
+ System.assertEquals( null, got, 'get, when called with a max age, will return null' );
+ }
+
+ @isTest
+ private static void get_minDate_whenCalled_returnsNull() // NOPMD: Test method name format
+ {
+ NullCache cache = new NullCache();
+ Object got = cache.get( null, NULL_MIN_DATE_TIME );
+ System.assertEquals( null, got, 'get, when called with a min datetime, will return null' );
+ }
+
@isTest
private static void put_whenCalled_doesNothing() // NOPMD: Test method name format
{
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/OrgCacheTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/OrgCacheTest.cls
index 9c2559f7f3a..ed7b2506daa 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/OrgCacheTest.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/OrgCacheTest.cls
@@ -2,6 +2,9 @@
private without sharing class OrgCacheTest
{
private final static Integer DEFAULT_LIFESPAN = 1000;
+ private final static DateTime BASE_TIME = DateTime.newInstanceGmt( 1974, 08, 24, 22, 45, 00 );
+ private final static Long NULL_AGE = null;
+ private final static DateTime NULL_MIN_DATE_TIME = null;
@isTest
private static void hasAccessToCache_whenCalledByUserWithCacheAccess_returnsTrue() // NOPMD: Test method name format
@@ -88,6 +91,251 @@ private without sharing class OrgCacheTest
System.assertEquals( expected, got, 'get, when called with a key that is in the cache, will return it' );
}
+ @isTest
+ private static void get_whenTheCachedValueHasNoAge_returnsTheValue() // NOPMD: Test method name format
+ {
+ setupAccessToSoqlCache( true );
+ String cachedObject = 'thecachedthing';
+
+ OrgCache cacheInstance = new OrgCache();
+
+ Cache.Org.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject );
+
+ Test.startTest();
+ Object got = cacheInstance.get( 'key' );
+ Test.stopTest();
+
+ System.assertEquals( cachedObject, got, 'get, when the cached value has no age, will return the value' );
+ }
+
+ @isTest
+ private static void get_maxAge_whenTheEntryIsTooOld_returnsNull() // NOPMD: Test method name format
+ {
+ setupAccessToSoqlCache( true );
+ String cachedObject = 'thecachedthing';
+
+ OrgCache cache = new OrgCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ TestDateTimeUtils.addToCurrentTime( 100 );
+ Object got = cache.get( 'key', 50 );
+ Test.stopTest();
+
+ System.assertEquals( null, got, 'get, passing maximumAge, when the entry is older than the stated number of seconds, will return null' );
+ }
+
+ @isTest
+ private static void get_maxAge_whenTheEntryIsExactlyRight_returnsTheValue() // NOPMD: Test method name format
+ {
+ setupAccessToSoqlCache( true );
+ String cachedObject = 'thecachedthing';
+
+ OrgCache cache = new OrgCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ TestDateTimeUtils.addToCurrentTime( 100 );
+ Object got = cache.get( 'key', 100 );
+ Test.stopTest();
+
+ System.assertEquals( cachedObject, got, 'get, passing maximumAge, when the entry is exactly the stated number of seconds, will return the cached value' );
+ }
+
+ @isTest
+ private static void get_maxAge_whenTheEntryIsYounger_returnsTheValue() // NOPMD: Test method name format
+ {
+ setupAccessToSoqlCache( true );
+ String cachedObject = 'thecachedthing';
+
+ OrgCache cache = new OrgCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ Object got = cache.get( 'key', 100 );
+ Test.stopTest();
+
+ System.assertEquals( cachedObject, got, 'get, passing maximumAge, when the entry is younger than the stated number of seconds, will return the cached value' );
+ }
+
+ @isTest
+ private static void get_maxAge_doesNotResetTheAge() // NOPMD: Test method name format
+ {
+ setupAccessToSoqlCache( true );
+ String cachedObject = 'thecachedthing';
+
+ OrgCache cache = new OrgCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ cache.get( 'key', 100 );
+
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ cache.get( 'key', 100 );
+
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ Object got = cache.get( 'key', 100 );
+ Test.stopTest();
+
+ System.assertEquals( null, got, 'get, passing maximumAge, does not reset the age of the cached entity, so when its too old it returns null' );
+ }
+
+ @isTest
+ private static void get_maxAge_whenNoAgeSpecifiedAndTheCachedValueHasNoAge_returnsTheValue() // NOPMD: Test method name format
+ {
+ setupAccessToSoqlCache( true );
+ String cachedObject = 'thecachedthing';
+
+ OrgCache cacheInstance = new OrgCache();
+
+ Cache.Org.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject );
+
+ Test.startTest();
+ Object got = cacheInstance.get( 'key', NULL_AGE );
+ Test.stopTest();
+
+ System.assertEquals( cachedObject, got, 'get, passing maximumAge, when no age is specified and the cached value has no age, will return the value' );
+ }
+
+ @isTest
+ private static void get_maxAge_whenAgeSpecifiedAndTheCachedValueHasNoAge_returnsNull() // NOPMD: Test method name format
+ {
+ setupAccessToSoqlCache( true );
+ String cachedObject = 'thecachedthing';
+
+ OrgCache cacheInstance = new OrgCache();
+
+ Cache.Org.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject );
+
+ Test.startTest();
+ Object got = cacheInstance.get( 'key', 100 );
+ Test.stopTest();
+
+ System.assertEquals( null, got, 'get, passing maximumAge, when an age is specified and the cached value has no age, will return null' );
+ }
+
+ @isTest
+ private static void get_minDateTime_whenTheEntryIsTooOld_returnsNull() // NOPMD: Test method name format
+ {
+ setupAccessToSoqlCache( true );
+ String cachedObject = 'thecachedthing';
+
+ OrgCache cache = new OrgCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ Object got = cache.get( 'key', BASE_TIME.addSeconds( 50 ) );
+ Test.stopTest();
+
+ System.assertEquals( null, got, 'get, passing minimumDateTime, when the entry is older than the stated time, will return null' );
+ }
+
+ @isTest
+ private static void get_minDateTime_whenTheEntryIsExactlyRight_returnsTheValue() // NOPMD: Test method name format
+ {
+ setupAccessToSoqlCache( true );
+ String cachedObject = 'thecachedthing';
+
+ OrgCache cache = new OrgCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ Object got = cache.get( 'key', BASE_TIME );
+ Test.stopTest();
+
+ System.assertEquals( cachedObject, got, 'get, passing minimumDateTime, when the entry is exactly the stated time, will return the cached value' );
+ }
+
+ @isTest
+ private static void get_minDateTime_whenTheEntryIsYounger_returnsTheValue() // NOPMD: Test method name format
+ {
+ setupAccessToSoqlCache( true );
+ String cachedObject = 'thecachedthing';
+
+ OrgCache cache = new OrgCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ Object got = cache.get( 'key', BASE_TIME.addSeconds( -50 ) );
+ Test.stopTest();
+
+ System.assertEquals( cachedObject, got, 'get, passing minimumDateTime, when the entry is younger than the stated time, will return the cached value' );
+ }
+
+ @isTest
+ private static void get_minDateTime_doesNotResetTheAge() // NOPMD: Test method name format
+ {
+ setupAccessToSoqlCache( true );
+ String cachedObject = 'thecachedthing';
+
+ OrgCache cache = new OrgCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ cache.get( 'key', BASE_TIME.addSeconds( -50 ) );
+
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ cache.get( 'key', BASE_TIME.addSeconds( -100 ) );
+
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ Object got = cache.get( 'key', BASE_TIME.addSeconds( 100 ) );
+ Test.stopTest();
+
+ System.assertEquals( null, got, 'get, passing minimumDateTime, does not reset the age of the cached entity, so when its too old it returns null' );
+ }
+
+ @isTest
+ private static void get_minDateTime_whenNoAgeSpecifiedAndTheCachedValueHasNoAge_returnsTheValue() // NOPMD: Test method name format
+ {
+ setupAccessToSoqlCache( true );
+ String cachedObject = 'thecachedthing';
+
+ OrgCache cacheInstance = new OrgCache();
+
+ Cache.Org.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject );
+
+ Test.startTest();
+ Object got = cacheInstance.get( 'key', NULL_MIN_DATE_TIME );
+ Test.stopTest();
+
+ System.assertEquals( cachedObject, got, 'get, passing minimumDateTime, when no time is specified and the cached value has no age, will return the value' );
+ }
+
+ @isTest
+ private static void get_minDateTime_whenAgeSpecifiedAndTheCachedValueHasNoAge_returnsNull() // NOPMD: Test method name format
+ {
+ setupAccessToSoqlCache( true );
+ String cachedObject = 'thecachedthing';
+
+ OrgCache cacheInstance = new OrgCache();
+
+ Cache.Org.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject );
+
+ Test.startTest();
+ Object got = cacheInstance.get( 'key', BASE_TIME.addSeconds( 100 ) );
+ Test.stopTest();
+
+ System.assertEquals( null, got, 'get, passing minimumDateTime, when a time is specified and the cached value has no age, will return null' );
+ }
@isTest
private static void put_whenTheUserDoesNotHaveAccessToTheCache_throwsAnException() // NOPMD: Test method name format
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/SessionCacheTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/SessionCacheTest.cls
index 15ede758276..ea8fbdcb4ec 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/SessionCacheTest.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/SessionCacheTest.cls
@@ -2,6 +2,9 @@
private without sharing class SessionCacheTest
{
private final static Integer DEFAULT_LIFESPAN = 1000;
+ private final static DateTime BASE_TIME = DateTime.newInstanceGmt( 1974, 08, 24, 22, 45, 00 );
+ private final static Long NULL_AGE = null;
+ private final static DateTime NULL_MIN_DATE_TIME = null;
@isTest
private static void hasAccessToCache_whenCalled_returnsTrue() // NOPMD: Test method name format
@@ -38,6 +41,224 @@ private without sharing class SessionCacheTest
System.assertEquals( expected, got, 'get, when called with a key that is in the cache, will return it' );
}
+ @isTest
+ private static void get_maxAge_whenTheEntryIsTooOld_returnsNull() // NOPMD: Test method name format
+ {
+ String cachedObject = 'thecachedthing';
+
+ SessionCache cache = new SessionCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ TestDateTimeUtils.addToCurrentTime( 100 );
+ Object got = cache.get( 'key', 50 );
+ Test.stopTest();
+
+ System.assertEquals( null, got, 'get, passing maximumAge, when the entry is older than the stated number of seconds, will return null' );
+ }
+
+ @isTest
+ private static void get_maxAge_whenTheEntryIsExactlyRight_returnsTheValue() // NOPMD: Test method name format
+ {
+ String cachedObject = 'thecachedthing';
+
+ SessionCache cache = new SessionCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ TestDateTimeUtils.addToCurrentTime( 100 );
+ Object got = cache.get( 'key', 100 );
+ Test.stopTest();
+
+ System.assertEquals( cachedObject, got, 'get, passing maximumAge, when the entry is exactly the stated number of seconds, will return the cached value' );
+ }
+
+ @isTest
+ private static void get_maxAge_whenTheEntryIsYounger_returnsTheValue() // NOPMD: Test method name format
+ {
+ String cachedObject = 'thecachedthing';
+
+ SessionCache cache = new SessionCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ Object got = cache.get( 'key', 100 );
+ Test.stopTest();
+
+ System.assertEquals( cachedObject, got, 'get, passing maximumAge, when the entry is younger than the stated number of seconds, will return the cached value' );
+ }
+
+ @isTest
+ private static void get_maxAge_doesNotResetTheAge() // NOPMD: Test method name format
+ {
+ String cachedObject = 'thecachedthing';
+
+ SessionCache cache = new SessionCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ cache.get( 'key', 100 );
+
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ cache.get( 'key', 100 );
+
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ Object got = cache.get( 'key', 100 );
+ Test.stopTest();
+
+ System.assertEquals( null, got, 'get, passing maximumAge, does not reset the age of the cached entity, so when its too old it returns null' );
+ }
+
+ @isTest
+ private static void get_maxAge_whenNoAgeSpecifiedAndTheCachedValueHasNoAge_returnsTheValue() // NOPMD: Test method name format
+ {
+ String cachedObject = 'thecachedthing';
+
+ SessionCache cacheInstance = new SessionCache();
+
+ Cache.Session.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject );
+
+ Test.startTest();
+ Object got = cacheInstance.get( 'key', NULL_AGE );
+ Test.stopTest();
+
+ System.assertEquals( cachedObject, got, 'get, passing maximumAge, when no age is specified and the cached value has no age, will return the value' );
+ }
+
+ @isTest
+ private static void get_maxAge_whenAgeSpecifiedAndTheCachedValueHasNoAge_returnsNull() // NOPMD: Test method name format
+ {
+ String cachedObject = 'thecachedthing';
+
+ SessionCache cacheInstance = new SessionCache();
+
+ Cache.Session.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject );
+
+ Test.startTest();
+ Object got = cacheInstance.get( 'key', 100 );
+ Test.stopTest();
+
+ System.assertEquals( null, got, 'get, passing maximumAge, when an age is specified and the cached value has no age, will return null' );
+ }
+
+ @isTest
+ private static void get_minDateTime_whenTheEntryIsTooOld_returnsNull() // NOPMD: Test method name format
+ {
+ String cachedObject = 'thecachedthing';
+
+ SessionCache cache = new SessionCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ Object got = cache.get( 'key', BASE_TIME.addSeconds( 50 ) );
+ Test.stopTest();
+
+ System.assertEquals( null, got, 'get, passing minimumDateTime, when the entry is older than the stated time, will return null' );
+ }
+
+ @isTest
+ private static void get_minDateTime_whenTheEntryIsExactlyRight_returnsTheValue() // NOPMD: Test method name format
+ {
+ String cachedObject = 'thecachedthing';
+
+ SessionCache cache = new SessionCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ Object got = cache.get( 'key', BASE_TIME );
+ Test.stopTest();
+
+ System.assertEquals( cachedObject, got, 'get, passing minimumDateTime, when the entry is exactly the stated time, will return the cached value' );
+ }
+
+ @isTest
+ private static void get_minDateTime_whenTheEntryIsYounger_returnsTheValue() // NOPMD: Test method name format
+ {
+ String cachedObject = 'thecachedthing';
+
+ SessionCache cache = new SessionCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ Object got = cache.get( 'key', BASE_TIME.addSeconds( -50 ) );
+ Test.stopTest();
+
+ System.assertEquals( cachedObject, got, 'get, passing minimumDateTime, when the entry is younger than the stated time, will return the cached value' );
+ }
+
+ @isTest
+ private static void get_minDateTime_doesNotResetTheAge() // NOPMD: Test method name format
+ {
+ String cachedObject = 'thecachedthing';
+
+ SessionCache cache = new SessionCache();
+
+ TestDateTimeUtils.setCurrentDateTime( BASE_TIME );
+ cache.put( 'key', cachedObject, DEFAULT_LIFESPAN );
+
+ Test.startTest();
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ cache.get( 'key', BASE_TIME.addSeconds( -50 ) );
+
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ cache.get( 'key', BASE_TIME.addSeconds( -100 ) );
+
+ TestDateTimeUtils.addToCurrentTime( 50 );
+ Object got = cache.get( 'key', BASE_TIME.addSeconds( 100 ) );
+ Test.stopTest();
+
+ System.assertEquals( null, got, 'get, passing minimumDateTime, does not reset the age of the cached entity, so when its too old it returns null' );
+ }
+
+ @isTest
+ private static void get_minDateTime_whenNoAgeSpecifiedAndTheCachedValueHasNoAge_returnsTheValue() // NOPMD: Test method name format
+ {
+ String cachedObject = 'thecachedthing';
+
+ SessionCache cacheInstance = new SessionCache();
+
+ Cache.Session.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject );
+
+ Test.startTest();
+ Object got = cacheInstance.get( 'key', NULL_MIN_DATE_TIME );
+ Test.stopTest();
+
+ System.assertEquals( cachedObject, got, 'get, passing minimumDateTime, when no time is specified and the cached value has no age, will return the value' );
+ }
+
+ @isTest
+ private static void get_minDateTime_whenAgeSpecifiedAndTheCachedValueHasNoAge_returnsNull() // NOPMD: Test method name format
+ {
+ String cachedObject = 'thecachedthing';
+
+ SessionCache cacheInstance = new SessionCache();
+
+ Cache.Session.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject );
+
+ Test.startTest();
+ Object got = cacheInstance.get( 'key', BASE_TIME.addSeconds( 100 ) );
+ Test.stopTest();
+
+ System.assertEquals( null, got, 'get, passing minimumDateTime, when a time is specified and the cached value has no age, will return null' );
+ }
+
+
@isTest
private static void put_whenCalledMultipleTimesWithTheSameKey_overwritesIt() // NOPMD: Test method name format
{
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DateLiterals.cls b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DateLiterals.cls
index 79fde39d568..1f59c3d128f 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DateLiterals.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DateLiterals.cls
@@ -19,7 +19,57 @@ public inherited sharing class ortoo_DateLiterals
private interface Literal extends fflib_Criteria.Literal {}
private interface Comparable extends fflib_Comparator.Comparable {}
- public static Date today = Date.today();
+ /**
+ * Representation of 'today' that is to be used whenever a reference is required.
+ * Should be used in place of System.today or Date.today
+ *
+ * The existance allows for the calendar to be re-set in Unit Tests.
+ */
+ public static Date today
+ {
+ get
+ {
+ if ( today != null )
+ {
+ return today;
+ }
+ return Date.today();
+ }
+ set;
+ }
+
+ /**
+ * Representation of 'now' that is to be used whenever a reference is required.
+ * Should be used in place of System.now or DateTime.now
+ *
+ * The existance allows for the clock to be re-set in Unit Tests.
+ */
+ public static DateTime now
+ {
+ get
+ {
+ if ( now != null )
+ {
+ return now;
+ }
+ return DateTime.now();
+ }
+ set;
+ }
+
+ /**
+ * Representation of 'now' as an epoch time in seconds.
+ *
+ * Since this depends on 'now', can be re-set in unit tests by chaning
+ * the underlying 'now'
+ */
+ public static Long epochTime
+ {
+ get
+ {
+ return DateTimeUtils.convertToEpochTime( now );
+ }
+ }
/**
* Dynamic representation of the date 'tomorrow', based on the same class's representation of 'today'
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DynamicSobjectSelector.cls b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DynamicSobjectSelector.cls
index fb424b01068..3a2a87d5155 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DynamicSobjectSelector.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DynamicSobjectSelector.cls
@@ -71,11 +71,11 @@ public inherited sharing class ortoo_DynamicSobjectSelector extends ortoo_Sobjec
}
/**
- * Retrieve the records that match the passed criteria, optionally via the Org Level SOQL Cache
+ * Retrieve the records that match the passed criteria, utilising the specified level of cache.
*
- * @param ortoo_Criteria The criteria that should be used to derive the records to return
- * @param Boolean Should the query go via the Org Level SOQL Cache?
- * @return List The result of the Selection
+ * @param ortoo_Criteria The criteria that should be used to derive the records to return
+ * @param CachedSoqlExecutor.CacheScope The scope of the cache that should be used
+ * @return List The result of the Selection
*/
public List selectByCriteria( ortoo_Criteria criteria, CachedSoqlExecutor.CacheScope cacheScope )
{
@@ -90,6 +90,52 @@ public inherited sharing class ortoo_DynamicSobjectSelector extends ortoo_Sobjec
.query( soql );
}
+ /**
+ * Retrieve the records that match the passed criteria, utilising the specified level of cache,
+ * only allowing data younger than the age specified to be retrieved from the cache.
+ *
+ * @param ortoo_Criteria The criteria that should be used to derive the records to return
+ * @param CachedSoqlExecutor.CacheScope The scope of the cache that should be used
+ * @param Long The maximum age, in seconds, that a cache entry is allowed to be before being discarded
+ * @return List The result of the Selection
+ */
+ public List selectByCriteria( ortoo_Criteria criteria, CachedSoqlExecutor.CacheScope cacheScope, Long maximumAgeInSeconds )
+ {
+ Contract.requires( criteria != null, 'selectByCriteria called with a null criteria' );
+ Contract.requires( cacheScope != null, 'selectByCriteria called with a null cacheScope' );
+ Contract.requires( maximumAgeInSeconds != null, 'selectByCriteria called with a null maximumAgeInSeconds' );
+ Contract.assert( sobjectType != null, 'selectByCriteria called when sobjectType has not been set' );
+
+ String soql = generateSoqlByCriteria( criteria );
+
+ return ((CachedSoqlExecutor)Application.APP_LOGIC.newInstance( CachedSoqlExecutor.class ))
+ .setScope( cacheScope )
+ .query( soql, maximumAgeInSeconds );
+ }
+
+ /**
+ * Retrieve the records that match the passed criteria, utilising the specified level of cache,
+ * only allowing data younger than the age specified to be retrieved from the cache.
+ *
+ * @param ortoo_Criteria The criteria that should be used to derive the records to return
+ * @param CachedSoqlExecutor.CacheScope The scope of the cache that should be used
+ * @param DateTime The minimum datetime (how recent) that a cache entry must have been created on to be used
+ * @return List The result of the Selection
+ */
+ public List selectByCriteria( ortoo_Criteria criteria, CachedSoqlExecutor.CacheScope cacheScope, DateTime minimumDateTimeAdded )
+ {
+ Contract.requires( criteria != null, 'selectByCriteria called with a null criteria' );
+ Contract.requires( cacheScope != null, 'selectByCriteria called with a null cacheScope' );
+ Contract.requires( minimumDateTimeAdded != null, 'selectByCriteria called with a null minimumDateTimeAdded' );
+ Contract.assert( sobjectType != null, 'selectByCriteria called when sobjectType has not been set' );
+
+ String soql = generateSoqlByCriteria( criteria );
+
+ return ((CachedSoqlExecutor)Application.APP_LOGIC.newInstance( CachedSoqlExecutor.class ))
+ .setScope( cacheScope )
+ .query( soql, minimumDateTimeAdded );
+ }
+
/**
* Request that the cached results for the given criteria be cleared.
* Assumes that the fields are set up correctly.
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DateLiteralsTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DateLiteralsTest.cls
index 8572ccac3bd..e9230d17293 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DateLiteralsTest.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DateLiteralsTest.cls
@@ -1,4 +1,3 @@
-
@isTest
private without sharing class ortoo_DateLiteralsTest
{
@@ -9,6 +8,56 @@ private without sharing class ortoo_DateLiteralsTest
System.assertEquals( Date.today(), got, 'today is set to today' );
}
+ @isTest
+ private static void today_whenSet_returnsThatDate() // NOPMD: Test method name format
+ {
+ Date expected = Date.newInstance( 2020, 5, 1 );
+ ortoo_DateLiterals.today = expected;
+
+ Date got = ortoo_DateLiterals.today;
+ System.assertEquals( expected, got, 'today, when set, will then be set to that value' );
+ }
+
+ @isTest
+ private static void now_returnsTheTimeForNow() // NOPMD: Test method name format
+ {
+ DateTime got = ortoo_DateLiterals.now;
+ DateTime expected = System.now();
+
+ System.assert( got > expected.addSeconds( -3 ) && got <= expected, 'now is set to now' );
+ }
+
+ @isTest
+ private static void now_whenSet_returnsThatDateTime() // NOPMD: Test method name format
+ {
+ DateTime expected = DateTime.newInstance( 2020, 5, 1, 13, 45, 0 );
+ ortoo_DateLiterals.now = expected;
+
+ DateTime got = ortoo_DateLiterals.now;
+ System.assertEquals( expected, got, 'now, when set, will then be set to that value' );
+ }
+
+ @isTest
+ private static void epochTime_returnsTheEpochTimeForNow() // NOPMD: Test method name format
+ {
+ Long expected = DateTimeUtils.convertToEpochTime( System.now() );
+
+ Long got = ortoo_DateLiterals.epochTime;
+ System.assert( got > expected - 3 && got <= expected, 'epochTime is set to now in seconds' );
+ }
+
+ @isTest
+ private static void epochTime_whenNowSet_returnsTheEpochForThatTime() // NOPMD: Test method name format
+ {
+ DateTime expectedDateTime = DateTime.newInstanceGmt( 2020, 5, 1, 13, 45, 0 );
+ Long expectedEpoch = 1588340700;
+
+ ortoo_DateLiterals.now = expectedDateTime;
+
+ Long got = ortoo_DateLiterals.epochTime;
+ System.assertEquals( expectedEpoch, got, 'epochTime, when now is set, will then be set to the epoch for that value' );
+ }
+
@isTest
private static void yesterday_returnYesterdayBasedOnTheConfiguredToday() // NOPMD: Test method name format
{
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DynamicSobjectSelectorTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DynamicSobjectSelectorTest.cls
index a23b7c31433..90ee2acf240 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DynamicSobjectSelectorTest.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DynamicSobjectSelectorTest.cls
@@ -123,6 +123,62 @@ private without sharing class ortoo_DynamicSobjectSelectorTest
System.assertEquals( expects, returnedSobjects, 'selectByCriteria, when not told about cache, will tell the CachedSoqlExecutor to use no cache' );
}
+ @isTest
+ private static void selectByCriteria_whenGivenAMaximumAge_willPassThatIntoTheQueryCall() // NOPMD: Test method name format
+ {
+ Long maximumAge = 100;
+
+ ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector()
+ .setSobjectType( Contact.sobjectType );
+
+ List expects = new List{ new Contact() };
+ Amoss_Instance cacheController = ApplicationMockRegistrar.registerMockAppLogic( CachedSoqlExecutor.class );
+ cacheController
+ .allows( 'setScope' )
+ .returning( cacheController.getDouble() )
+ .also()
+ .expects( 'query' )
+ .withParameter().containing( 'SELECT Id FROM Contact' )
+ .thenParameter( maximumAge )
+ .returning( expects );
+
+ Test.startTest();
+ List returnedSobjects = selector.selectByCriteria( new ortoo_Criteria(), CachedSoqlExecutor.CacheScope.ORG, maximumAge );
+ Test.stopTest();
+
+ cacheController.verify();
+
+ System.assertEquals( expects, returnedSobjects, 'selectByCriteria, when given a maximum age, will pass that into the query call' );
+ }
+
+ @isTest
+ private static void selectByCriteria_whenGivenAMinimumDate_willPassThatIntoTheQueryCall() // NOPMD: Test method name format
+ {
+ DateTime minimumDate = DateTime.newInstance( 2020, 01, 02 );
+
+ ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector()
+ .setSobjectType( Contact.sobjectType );
+
+ List expects = new List{ new Contact() };
+ Amoss_Instance cacheController = ApplicationMockRegistrar.registerMockAppLogic( CachedSoqlExecutor.class );
+ cacheController
+ .allows( 'setScope' )
+ .returning( cacheController.getDouble() )
+ .also()
+ .expects( 'query' )
+ .withParameter().containing( 'SELECT Id FROM Contact' )
+ .thenParameter( minimumDate )
+ .returning( expects );
+
+ Test.startTest();
+ List returnedSobjects = selector.selectByCriteria( new ortoo_Criteria(), CachedSoqlExecutor.CacheScope.ORG, minimumDate );
+ Test.stopTest();
+
+ cacheController.verify();
+
+ System.assertEquals( expects, returnedSobjects, 'selectByCriteria, when given a minimum date, will pass that into the query call' );
+ }
+
@isTest
private static void selectByCriteria_whenTheSobjectTypeHasNotBeenSet_willThrowAnException() // NOPMD: Test method name format
{
@@ -164,6 +220,115 @@ private without sharing class ortoo_DynamicSobjectSelectorTest
ortoo_Asserts.assertContains( 'selectByCriteria called with a null criteria', exceptionMessage, 'selectByCriteria, when given a null criteria, will throw an exception' );
}
+ @isTest
+ private static void selectByCriteria_whenPassedANullScope_throwsAnException() // NOPMD: Test method name format
+ {
+ ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector()
+ .setSobjectType( Contact.sobjectType );
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ selector.selectByCriteria( new ortoo_Criteria(), null );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ ortoo_Asserts.assertContains( 'selectByCriteria called with a null cacheScope', exceptionMessage, 'selectByCriteria, when passed a null cacheScope, will throw an exception' );
+ }
+
+ @isTest
+ private static void selectByCriteria_whenPassedANullScopeAnMaxAge_throwsAnException() // NOPMD: Test method name format
+ {
+ ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector()
+ .setSobjectType( Contact.sobjectType );
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ selector.selectByCriteria( new ortoo_Criteria(), null, 12 );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ ortoo_Asserts.assertContains( 'selectByCriteria called with a null cacheScope', exceptionMessage, 'selectByCriteria, when passed a null cacheScope with a max age, will throw an exception' );
+ }
+
+ @isTest
+ private static void selectByCriteria_whenPassedANullScopeAnMinDate_throwsAnException() // NOPMD: Test method name format
+ {
+ ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector()
+ .setSobjectType( Contact.sobjectType );
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ selector.selectByCriteria( new ortoo_Criteria(), null, DateTime.newInstance( 2020, 2, 1 ) );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ ortoo_Asserts.assertContains( 'selectByCriteria called with a null cacheScope', exceptionMessage, 'selectByCriteria, when passed a null cacheScope with a min date, will throw an exception' );
+ }
+
+ @isTest
+ private static void selectByCriteria_whenPassedANullMaxAge_throwsAnException() // NOPMD: Test method name format
+ {
+ ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector()
+ .setSobjectType( Contact.sobjectType );
+
+ Long nullAge;
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ selector.selectByCriteria( new ortoo_Criteria(), CachedSoqlExecutor.CacheScope.ORG, nullAge );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ ortoo_Asserts.assertContains( 'selectByCriteria called with a null maximumAgeInSeconds', exceptionMessage, 'selectByCriteria, when passed a null max age, will throw an exception' );
+ }
+
+ @isTest
+ private static void selectByCriteria_whenPassedANullMinDate_throwsAnException() // NOPMD: Test method name format
+ {
+ ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector()
+ .setSobjectType( Contact.sobjectType );
+
+ DateTime nullDateTime;
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ selector.selectByCriteria( new ortoo_Criteria(), CachedSoqlExecutor.CacheScope.ORG, nullDateTime );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ ortoo_Asserts.assertContains( 'selectByCriteria called with a null minimumDateTimeAdded', exceptionMessage, 'selectByCriteria, when passed a null min date, will throw an exception' );
+ }
+
@isTest
private static void addField_whenGivenAString_willAddThatFieldToTheSelection() // NOPMD: Test method name format
{
diff --git a/framework/default/ortoo-core/default/classes/test-utils/TestDateTimeUtils.cls b/framework/default/ortoo-core/default/classes/test-utils/TestDateTimeUtils.cls
new file mode 100644
index 00000000000..471e084f924
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/test-utils/TestDateTimeUtils.cls
@@ -0,0 +1,46 @@
+/**
+ * Utility class that provides the ability to generate Ids for given SObject types in tests
+ *
+ * @group Utils
+ */
+@isTest
+public inherited sharing class TestDateTimeUtils
+{
+ /**
+ * Sets the current Date as returned by ortoo_DateLiterals.today
+ * Does not change the return of ortoo_DateLiterals.now
+ */
+ public static void setCurrentDate( Date currentDate )
+ {
+ ortoo_DateLiterals.today = currentDate;
+ ortoo_DateLiterals.now = DateTimeUtils.shiftToDate( ortoo_DateLiterals.now, currentDate );
+ }
+
+ /**
+ * Sets the current DateTime as returned by ortoo_DateLiterals.now
+ * Also sets ortoo_DateLiterals.today to be for the same date
+ */
+ public static void setCurrentDateTime( DateTime currentDateTime )
+ {
+ ortoo_DateLiterals.now = currentDateTime;
+ ortoo_DateLiterals.today = DateTimeUtils.convertToDate( currentDateTime );
+ }
+
+ /**
+ * Sets the current DateTime as returned by ortoo_DateLiterals.now to the time represented by the given epoch
+ * Also sets ortoo_DateLiterals.today to be for the same date
+ */
+ public static void setCurrentDateTime( Long epochInSeconds )
+ {
+ setCurrentDateTime( DateTimeUtils.convertToDateTime( epochInSeconds ) );
+ }
+
+ /**
+ * Adds the given number of seconds to ortoo_DateLiterals.now
+ * Also sets ortoo_DateLiterals.today to be for the same date
+ */
+ public static void addToCurrentTime( Integer numberOfSeconds )
+ {
+ setCurrentDateTime( ortoo_DateLiterals.epochTime + numberOfSeconds );
+ }
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/test-utils/TestDateTimeUtils.cls-meta.xml b/framework/default/ortoo-core/default/classes/test-utils/TestDateTimeUtils.cls-meta.xml
new file mode 100644
index 00000000000..dd61d1f917e
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/test-utils/TestDateTimeUtils.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 52.0
+ Active
+
diff --git a/framework/default/ortoo-core/default/classes/utils/DateTimeUtils.cls b/framework/default/ortoo-core/default/classes/utils/DateTimeUtils.cls
index e1c043b9ea2..e3c4eedefd4 100644
--- a/framework/default/ortoo-core/default/classes/utils/DateTimeUtils.cls
+++ b/framework/default/ortoo-core/default/classes/utils/DateTimeUtils.cls
@@ -6,13 +6,59 @@
public inherited sharing class DateTimeUtils
{
/**
- * Returns the current Epoch Time, in seconds
+ * Given a date time, will convert it into the epoch time, in seconds
*
- * @return Long The current Epoch Time, in seconds
+ * @param DateTime The time to convert
+ * @return Long The epoch time, in number of seconds
*/
- public static Long getEpochTime()
+ public static Long convertToEpochTime( DateTime timeToConvert )
{
- Long millisecondsNow = Datetime.now().getTime();
- return millisecondsNow / 1000;
+ Contract.requires( timeToConvert != null, 'convertToEpochTime called with a null timeToConvert' );
+
+ Long milliseconds = timeToConvert.getTime();
+ return milliseconds / 1000;
}
-}
+
+ /**
+ * Given a DateTime, will convert it to a date
+ *
+ * @param DateTime The time to convert
+ * @return Date The resulting Date
+ */
+ public static Date convertToDate( DateTime timeToConvert )
+ {
+ Contract.requires( timeToConvert != null, 'convertToDate called with a null timeToConvert' );
+
+ return Date.newInstance( timeToConvert.year(), timeToConvert.month(), timeToConvert.day() );
+ }
+
+ /**
+ * Given a DateTime, will shift it to the specified date, keeping the time the same.
+ *
+ * Note that the millisecond precision is lost
+ *
+ * @param DateTime The time to shift
+ * @param Date The new date to shift the DateTime to
+ * @return DateTime The resulting DateTime
+ */
+ public static DateTime shiftToDate( DateTime timeToShift, Date newDate )
+ {
+ Contract.requires( timeToShift != null, 'shiftToDate called with a null timeToShift' );
+ Contract.requires( newDate != null, 'shiftToDate called with a null newDate' );
+
+ return DateTime.newInstance( newDate.year(), newDate.month(), newDate.day(), timeToShift.hour(), timeToShift.minute(), timeToShift.second() );
+ }
+
+ /**
+ * Given an epoch time in seconds, will convert it into a DateTime
+ *
+ * @param Long The epoch time, in number of seconds
+ * @return DateTime The resulting DateTime
+ */
+ public static DateTime convertToDateTime( Long epochInSeconds )
+ {
+ Contract.requires( epochInSeconds != null, 'convertToDateTime called with a null epochInSeconds' );
+
+ return DateTime.newInstance( epochInSeconds * 1000 );
+ }
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/utils/tests/DateTimeUtilsTest.cls b/framework/default/ortoo-core/default/classes/utils/tests/DateTimeUtilsTest.cls
index c947793b716..8a1128b5b6a 100644
--- a/framework/default/ortoo-core/default/classes/utils/tests/DateTimeUtilsTest.cls
+++ b/framework/default/ortoo-core/default/classes/utils/tests/DateTimeUtilsTest.cls
@@ -1,18 +1,151 @@
@isTest
public with sharing class DateTimeUtilsTest
{
- @isTest
- private static void getEpochTime_whenCalled_returnsEpochInSeconds() // NOPMD: Test method name format
- {
- Test.startTest();
- Long got = DateTimeUtils.getEpochTime();
- Test.stopTest();
-
- Integer e9 = Integer.valueOf( got / 1000000000 ); // if you divide the epoch seconds by 10^9, you should get 1 if the date is later than 9th September 2001
- Integer e10 = Integer.valueOf( ( got / 1000000000 ) / 10 ); // if you divide the epoch seconds by 10^10, you should get < 1 if the date is earlier than 20th November 2286
- // need to do it in two steps becayse 10^10 is not a valid integer
-
- System.assert( e9 > 0, 'getEpochTime, when called, will return the epoch time in seconds - being greater than 10^9' );
- System.assertEquals( 0, e10, 'getEpochTime, when called, will return the epoch time in seconds - being less than 10^10' );
- }
-}
+ @isTest
+ private static void convertToEpochTime_whenGivenADateTime_convertsItToEpochInSeconds() // NOPMD: Test method name format
+ {
+ DateTime timeToConvert = DateTime.newInstanceGmt( 2020, 5, 1, 13, 45, 0 );
+ Long expected = 1588340700;
+
+ Test.startTest();
+ Long got = DateTimeUtils.convertToEpochTime( timeToConvert );
+ Test.stopTest();
+
+ System.assertEquals( expected, got, 'convertToEpochTime, when given a date time, will convert it to epoch in seconds' );
+ }
+
+ @isTest
+ private static void convertToEpochTime_whenPassedANullTimeToConvert_throwsAnException() // NOPMD: Test method name format
+ {
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ DateTimeUtils.convertToEpochTime( null );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ ortoo_Asserts.assertContains( 'convertToEpochTime called with a null timeToConvert', exceptionMessage, 'convertToEpochTime, when passed a null timeToConvert, will throw an exception' );
+ }
+
+ @isTest
+ private static void convertToDate_whenGivenADateTime_returnsTheDateFromIt() // NOPMD: Test method name format
+ {
+ DateTime timeToConvert = DateTime.newInstance( 2020, 5, 1, 13, 45, 0 );
+ Date expected = Date.newInstance( 2020, 5, 1 );
+
+ Test.startTest();
+ Date got = DateTimeUtils.convertToDate( timeToConvert );
+ Test.stopTest();
+
+ System.assertEquals( expected, got, 'convertToDate, when given a date time, will return the date from it' );
+ }
+
+ @isTest
+ private static void convertToDate_whenPassedANullTimeToConvert_throwsAnException() // NOPMD: Test method name format
+ {
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ DateTimeUtils.convertToDate( null );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ ortoo_Asserts.assertContains( 'convertToDate called with a null timeToConvert', exceptionMessage, 'convertToDate, when passed a null timeToConvert, will throw an exception' );
+ }
+
+ @isTest
+ private static void shiftToDate_whenGivenADateTimeAndDate_returnsANewDateTimeForTheNewDate() // NOPMD: Test method name format
+ {
+ DateTime timeToShift = DateTime.newInstance( 2020, 5, 1, 13, 45, 0 );
+ Date newDate = Date.newInstance( 2021, 7, 11 );
+
+ DateTime expected = DateTime.newInstance( 2021, 7, 11, 13, 45, 0 );
+
+ Test.startTest();
+ DateTime got = DateTimeUtils.shiftToDate( timeToShift, newDate );
+ Test.stopTest();
+
+ System.assertEquals( expected, got, 'shiftToDate, when given a DateTime and a Date, will return a new DateTime on the given date, with the original DateTime time' );
+ }
+
+ @isTest
+ private static void shiftToDate_whenPassedANullTimeToShift_throwsAnException() // NOPMD: Test method name format
+ {
+ Date newDate = Date.newInstance( 2021, 7, 11 );
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ DateTimeUtils.shiftToDate( null, newDate );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ ortoo_Asserts.assertContains( 'shiftToDate called with a null timeToShift', exceptionMessage, 'shiftToDate, when passed a null timeToShift, will throw an exception' );
+ }
+
+ @isTest
+ private static void shiftToDate_whenPassedANullNewDate_throwsAnException() // NOPMD: Test method name format
+ {
+ DateTime timeToShift = DateTime.newInstance( 2020, 5, 1, 13, 45, 0 );
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ DateTimeUtils.shiftToDate( timeToShift, null );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ ortoo_Asserts.assertContains( 'shiftToDate called with a null newDate', exceptionMessage, 'shiftToDate, when passed a null newDate, will throw an exception' );
+ }
+
+ @isTest
+ private static void convertToDateTime_whenGivenAEpochInSeconds_convertsItToDateTime() // NOPMD: Test method name format
+ {
+ Long epochInSeconds = 1588340700;
+ DateTime expected = DateTime.newInstanceGmt( 2020, 5, 1, 13, 45, 0 );
+
+ Test.startTest();
+ DateTime got = DateTimeUtils.convertToDateTime( epochInSeconds );
+ Test.stopTest();
+
+ System.assertEquals( expected, got, 'convertToDateTime, when given an epoch time in seconds, will convert it to DateTime' );
+ }
+
+ @isTest
+ private static void convertToDateTime_whenPassedANullEpochInSeconds_throwsAnException() // NOPMD: Test method name format
+ {
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ DateTimeUtils.convertToDateTime( null );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ ortoo_Asserts.assertContains( 'convertToDateTime called with a null epochInSeconds', exceptionMessage, 'convertToDateTime, when passed a null epochInSeconds, will throw an exception' );
+ }
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-lwc-list-view-buttons/classes/AbstractRedirectToLwcTabController.cls b/framework/default/ortoo-lwc-list-view-buttons/classes/AbstractRedirectToLwcTabController.cls
index 274ab7a9cca..a714b72aa24 100644
--- a/framework/default/ortoo-lwc-list-view-buttons/classes/AbstractRedirectToLwcTabController.cls
+++ b/framework/default/ortoo-lwc-list-view-buttons/classes/AbstractRedirectToLwcTabController.cls
@@ -64,7 +64,7 @@ public virtual with sharing class AbstractRedirectToLwcTabController
{
get
{
- return String.valueOf( DateTimeUtils.getEpochTime() );
+ return String.valueOf( ortoo_DateLiterals.epochTime );
}
}
diff --git a/framework/default/ortoo-lwc-list-view-buttons/classes/TimeController.cls b/framework/default/ortoo-lwc-list-view-buttons/classes/TimeController.cls
index 4f57bd26fbd..5512842e7cf 100644
--- a/framework/default/ortoo-lwc-list-view-buttons/classes/TimeController.cls
+++ b/framework/default/ortoo-lwc-list-view-buttons/classes/TimeController.cls
@@ -12,6 +12,6 @@ public with sharing class TimeController
@AuraEnabled
public static Long getEpochTime()
{
- return DateTimeUtils.getEpochTime();
+ return ortoo_DateLiterals.epochTime;
}
}
\ No newline at end of file