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