From 14fc5973a44cae2d2212313a59c76162c3820d81 Mon Sep 17 00:00:00 2001 From: Robert Baillie Date: Tue, 26 Apr 2022 16:08:04 +0100 Subject: [PATCH] Added tests for OrgCache ageing and improved behaviours --- TODO.txt | 10 + .../fflib-extension/caching/CacheEntry.cls | 11 +- .../fflib-extension/caching/OrgCache.cls | 47 ++-- .../caching/tests/OrgCacheTest.cls | 255 +++++++++++++++++- .../fflib-extension/ortoo_DateLiterals.cls | 30 ++- .../classes/test-utils/TestDateTimeUtils.cls | 28 ++ .../test-utils/TestDateTimeUtils.cls-meta.xml | 5 + .../default/classes/utils/DateTimeUtils.cls | 8 + 8 files changed, 361 insertions(+), 33 deletions(-) create mode 100644 framework/default/ortoo-core/default/classes/test-utils/TestDateTimeUtils.cls create mode 100644 framework/default/ortoo-core/default/classes/test-utils/TestDateTimeUtils.cls-meta.xml diff --git a/TODO.txt b/TODO.txt index 1ff27df0c07..31969268f02 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1,13 @@ +TODO: + * Combine the DateTimeUtils and DateLiterals where appropriate + * Document TestDateTimeUtils and how to set now and today + * Test ageing using dates on the OrgCache + * Implement ageing into the SessionCache + * Add the aged get definitions into ICacheAdaptor + * Add 'maximum age' into CachedSoqlExecutor + + + * Move all the Jest LWc tests over to create the component in the before Look at the use of 'MockDatabase' in fflib 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 index 0b8712c966c..a6cdd75f382 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/CacheEntry.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/CacheEntry.cls @@ -5,11 +5,11 @@ public inherited sharing class CacheEntry Long createdOnEpoch; Object value; - private Long age + private Long ageInSeconds { get { - return DateTimeUtils.getEpochTime() - this.createdOnEpoch ; + return DateTimeUtils.getEpochTime() - this.createdOnEpoch; } } @@ -19,10 +19,11 @@ public inherited sharing class CacheEntry this.createdOnEpoch = DateTimeUtils.getEpochTime(); } - public Boolean isYoungerThan( Integer compareAgeInSeconds ) + public Boolean isYoungerThanOrEqualTo( Long compareAgeInSeconds ) { - Contract.requires( compareAgeInSeconds != null, 'isOlderThan called with a null compareAgeInSeconds' ); - return age < compareAgeInSeconds; + Contract.requires( compareAgeInSeconds != null, 'isYoungerThanOrEqualTo called with a null compareAgeInSeconds' ); + + return ageInSeconds <= compareAgeInSeconds; } public Object getValue() 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 6a4babf64e5..1749af0f352 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 @@ -57,29 +57,31 @@ public inherited sharing class OrgCache implements ICacheAdaptor */ public Object get( String key ) { - Contract.requires( String.isNotBlank( key ), 'get called with a blank key' ); - - if ( ! hasAccessToCache ) - { - throw new AccessViolationException( Label.ortoo_core_org_cache_access_violation ) - .setErrorCode( FrameworkErrorCodes.CACHE_ACCESS_VIOLATION ) - .addContext( 'method', 'get' ) - .addContext( 'key', key ); - } - - Object rawCacheValue = Cache.Org.get( createFullyQualifiedKey( key ) ); + Long maximumAgeInSeconds = null; + return get( key, maximumAgeInSeconds ); + } - if ( rawCacheValue instanceOf CacheEntry ) + /** + * 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 ) { - return ((CacheEntry)rawCacheValue).getValue(); + maximumAgeInSeconds = DateTimeUtils.getEpochTime() - DateTimeUtils.getEpochTime( minimumDateTime ); } - return rawCacheValue; + return get( key, maximumAgeInSeconds ); } - // TODO: document - // TODO: test /** - * Retrieve the cache entry with the given key. + * 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 @@ -87,7 +89,7 @@ public inherited sharing class OrgCache implements ICacheAdaptor * @param String The key for the object to retrieve * @return Object The cached object, if it exists */ - public Object get( String key, Integer maximumAgeInSeconds ) + public Object get( String key, Long maximumAgeInSeconds ) { Contract.requires( String.isNotBlank( key ), 'get called with a blank key' ); @@ -105,11 +107,17 @@ public inherited sharing class OrgCache implements ICacheAdaptor if ( rawCacheValue instanceOf CacheEntry ) { CacheEntry cacheValue = (CacheEntry)rawCacheValue; - if ( cacheValue.isYoungerThan( maximumAgeInSeconds ) ) + 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 } @@ -232,6 +240,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/tests/OrgCacheTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/OrgCacheTest.cls index ac26df9e9f6..1112e87f32c 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,13 +2,9 @@ private without sharing class OrgCacheTest { private final static Integer DEFAULT_LIFESPAN = 1000; - - // if age on retrieve is too old, returns null - // if age on retrieve is exactly the right age, returns value - // if age on retrieve is young enough, returns value - // retrieving does not reset the age - // if object is not a CacheEntry and no age specified, returns value - // if object is not a CacheEntry and an age specified, returns null + 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_MAX_DATE_TIME = null; @isTest private static void hasAccessToCache_whenCalledByUserWithCacheAccess_returnsTrue() // NOPMD: Test method name format @@ -95,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_MAX_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/ortoo_DateLiterals.cls b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DateLiterals.cls index 9c440886749..6ce42936a58 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,10 +19,36 @@ 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(); + // TODO: document + // TODO: test + public static Date today + { + get + { + if ( today != null ) + { + return today; + } + return Date.today(); + } + set; + } + // TODO: document // TODO: test - public static DateTime now = DateTime.now(); + public static DateTime now + { + get + { + if ( now != null ) + { + return now; + } + return DateTime.now(); + } + set; + } + /** * Dynamic representation of the date 'tomorrow', based on the same class's representation of 'today' */ 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..9b0e1d86c92 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/test-utils/TestDateTimeUtils.cls @@ -0,0 +1,28 @@ +/** + * Utility class that provides the ability to generate Ids for given SObject types in tests + * + * @group Utils + */ +@isTest +public inherited sharing class TestDateTimeUtils +{ + public static void setCurrentDate( Date currentDate ) + { + ortoo_DateLiterals.today = currentDate; + } + + public static void setCurrentDateTime( DateTime currentDateTime ) + { + ortoo_DateLiterals.now = currentDateTime; + } + + public static void setCurrentDateTime( Long epochInSeconds ) + { + ortoo_DateLiterals.now = DateTimeUtils.convertToDateTime( epochInSeconds ); + } + + public static void addToCurrentTime( Integer numberOfSeconds ) + { + ortoo_DateLiterals.now = DateTimeUtils.convertToDateTime( DateTimeUtils.getEpochTime( ortoo_DateLiterals.now ) + 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 1fe05b029fe..c6956e70d18 100644 --- a/framework/default/ortoo-core/default/classes/utils/DateTimeUtils.cls +++ b/framework/default/ortoo-core/default/classes/utils/DateTimeUtils.cls @@ -10,6 +10,7 @@ public inherited sharing class DateTimeUtils * * @return Long The current Epoch Time, in seconds */ + // TODO: should this be on DateLiterals? public static Long getEpochTime() { return getEpochTime( ortoo_DateLiterals.now ); @@ -22,4 +23,11 @@ public inherited sharing class DateTimeUtils Long milliseconds = timeToConvert.getTime(); return milliseconds / 1000; } + + // TODO: test + // TODO: document + public static DateTime convertToDateTime( Long epochInSeconds ) + { + return DateTime.newInstance( epochInSeconds * 1000 ); + } }